vue mvvm数据响应实现


Posted in Javascript onNovember 11, 2020

为什么实现数据响应式

当前vue、react等框架流行。无论是vue、还是react框架大家最初的设计思路都是类似的。都是以数据驱动视图,数据优先。希望能够通过框架减少开发人员直接操作节点,让开发人员能够把更多的精力放在业务上而不是过多的放在操作节点上。另一方面,框架会通过虚拟dom及diff算法提高页面性能。这其中需要数据优先最根本的思路就是实现数据响应式。so,本次来看下如何基于原生实现数据响应式。

vue中的数据响应

vue中会根据数据将数据通过大胡子语法及指令渲染到视图上,这里我们以大胡子语法为例。如下:

<div id="app">
    {{message}}
</div>
let vm = new Vue({
  el:"#app",
  data:{
    message:"测试数据"
  }
})
setTimeout(()=>{
  vm.message = "修改的数据";
},1000)

如上代码,很简单 。vue做了两件事情。一、把message数据初次渲染到视图。二、当message数据改变的时候视图上渲染的message数据同时也会做出响应。以最简单的案例。带着问题来看,通过原生js如何实现??这里为了简化操作便于理解,这里就不去使用虚拟dom。直接操作dom结构。

实现数据初次渲染

根据vue调用方式。定义Vue类来实现各种功能。将初次渲染过程定义成编译compile函数渲染视图。通过传入的配置以及操作dom来实现渲染。大概思路是通过正则查找html 里 #app 作用域内的表达式,然后查找数据做对应的替换即可。具体实现如下:

class Vue {
  constructor(options) {
    this.opts = options;
    this.compile();
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
        }
      }
    })
  }
}

如上完成了初次渲染,将message数据渲染到了视图上。但是会返现并没对深层次的dom结构做处理也就是如下情况:

<div id="app">
    1{{ message }}2
    <div>
      hello , {{ message }}
    </div>
  </div>

vue mvvm数据响应实现

渲染结果如上

发现结果并没有达到预期。so,需要改下代码,让节点可以深层次查找就可以了。代码如下:

compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }

上述代码通过递归查找节点 实现深层次节点的渲染工作。如此,就实现了视图的初次渲染。

数据劫持

回过头来看下上面说的第二个问题:当message数据改变的时候视图上渲染的message数据同时也会做出响应。如何实现数据响应式?简而言之就是数据变动影响视图变动?再将问题拆分下 1. 如何知道数据变动了? 2.如何根据数据变动来更改视图?

  • 如何知道数据变动了? 这里就需要用到数据拦截了,或者叫数据观察。把会变动的data数据观察起来。当他变动的时候我们可以做后续的渲染事情。如何拦截数据呢 ?vue2里采取的是definePrototype。
let obj = {
  myname:"张三"
}
Object.defineProperty(obj,'myname',{
  configurable:true,
  enumerable:true,
  get(){
    console.log("get.")
    return "张三";
  },
  set(newValue){
    console.log("set")
    console.log(newValue);
  }
})
console.log(obj);

上述代码会发现,通过defineProperty劫持的对象属性下都会有get及set方法。那么当我们获取或者设置数据的时候就能出发对应的get及set 。这样就能拦截数据做后续操作。

vue mvvm数据响应实现

还有没有其他方式达到数据劫持的效果呢?ES6中出现了Proxy 代理对象同样也可以达到类似劫持数据的功能。如下代码:

let obj = {
  myname:"张三"
}
let newObj = new Proxy(obj,{
  get(target,key){
    console.log("get...")
    return "张三"
  },
  set(target,name,newValue){
    console.log("set...");
    return Reflect.set(target,name,newValue);
  }
})

两种方式都可以实现数据劫持。proxy功能更加强大,很多方法是defineProperty所不具备的。且proxy直接拦截的是对象而defineProperty拦截的是对象属性。so,可以利用上述方式将data数据做劫持,代码如下:

observe(data){
    let keys = Object.keys(data);
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          return value;
        },
        set(newValue){
          value = newValue;
        }
      });
    })
 }

观察者模式实现数据响应

有了劫持数据方式后,接下来需要实现的就是当修改数据的时候将新数据渲染到视图。如何办到呢?会发现,需要在data设置的时候触发视图的compile编译。二者之间互相影响,此时可以想到利用观察者模式,通过观察者模式让二者产生关联,如下:

vue mvvm数据响应实现

图略小,代码也贴上吧。

class Vue extends EventTarget {
  constructor(options) {
    super();
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          return value;
        },
        set(newValue){
          _this.dispatchEvent(new CustomEvent(key,{
            detail:newValue
          }));
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
          this.addEventListener($1,e=>{
            let oldValue = this.opts.data[$1];
            let newValue = e.detail;
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

如上,成功的通过观察者模式实现了数据的响应。但是会发现data与compile之间需要通过键名来进行关联。如果data数据结构嵌套关系复杂后面会比较难处理。有没有一种方式让二者松解耦呢?这时候可以用发布订阅模式来进行改造。

发布订阅模式改造响应式

vue mvvm数据响应实现

还是略小,也还是贴上代码:

class Vue {
  constructor(options) {
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      let dep = new Dep();
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          if(Dep.target){
            dep.addSub(Dep.target); 
          }
          return value;
        },
        set(newValue){
          dep.notify(newValue);
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
          new Watcher(this.opts.data,$1,(newValue)=>{
            let oldValue = this.opts.data[$1];
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

class Dep{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
  }
  notify(newValue){
    this.subs.forEach(sub=>{
      sub.update(newValue);
    })
  }
}

class Watcher{
  constructor(data,key,cb){
    Dep.target = this;
    data[key];
    this.cb = cb;
    Dep.target = null;
  }
  update(newValue){
    this.cb(newValue);
  }
}

如上代码思路是 针对每个数据会生成一个dep(依赖收集器)在数据get的时候收集watcher,将watcher 添加到dep里保存。数据一旦有改变触发notify发布消息从而影响compile编译更新视图。这个流程也可以参看下图:

vue mvvm数据响应实现

如上就完成了视图响应。通过上述代码,我们可以看出实现数据响应两个核心点1.数据劫持。2.观察者和发布订阅。在这我们可以思考一个问题,2个设计模式都是可以实现的但是有什么区别呢?

观察者与发布订阅

这里需要从概念来看

  • 观察者模式:定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新,一对多的关系。
  • 发布订阅模式:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

vue mvvm数据响应实现

两者之间关系,发布订阅是三者之间关系。发布订阅会多了一个关系器来组织主题和观察者之间的关系。这样做的好处就是松解耦。看上面响应式例子可以看出观察者需要通过事件名称来进行关联。发布订阅定义dep管理器之后data和compile彻底解耦,让二者松散解耦。在处理多层数据结构上发布订阅会更清晰。松解耦能够应对更多变化,把模块之间依赖降到最低。发布订阅广义上是观察者模式。

好了 暂时先over 。 如果觉得有收获的话可以点个赞,赠人玫瑰,手有余香!!!!

以上就是vue mvvm数据响应实现的详细内容,更多关于vue mvvm数据响应的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
JS定义回车事件(实现代码)
Jul 08 Javascript
javascript跨浏览器的属性判断方法
Mar 16 Javascript
js获取滚动距离的方法
May 30 Javascript
node.js实现端口转发
Apr 14 Javascript
JavaScript轮播图简单制作方法
Feb 20 Javascript
angular写一个列表的选择全选交互组件的示例
Jan 22 Javascript
vue项目实战总结篇
Feb 11 Javascript
vue中使用cropperjs的方法
Mar 01 Javascript
解决node修改后需频繁手动重启的问题
May 13 Javascript
小程序视频或音频自定义可拖拽进度条的示例代码
Sep 30 Javascript
前端Vue项目详解--初始化及导航栏
Jun 24 Javascript
vue 项目引入echarts 添加点击事件操作
Sep 09 Javascript
Js数组扁平化实现方法代码总汇
Nov 11 #Javascript
使用Vant完成通知栏Notify的提示操作
Nov 11 #Javascript
Vue3 响应式侦听与计算的实现
Nov 11 #Javascript
详解Vue.js3.0 组件是如何渲染为DOM的
Nov 10 #Javascript
在vs code 中如何创建一个自己的 Vue 模板代码
Nov 10 #Javascript
JavaScript中常用的3种弹出提示框(alert、confirm、prompt)
Nov 10 #Javascript
原生JS实现弹幕效果的简单操作指南
Nov 10 #Javascript
You might like
用PHP的ob_start();控制您的浏览器cache!
2006/11/25 PHP
PHP中对数组的一些常用的增、删、插操作函数总结
2015/11/27 PHP
php自动载入类用法实例分析
2016/06/24 PHP
Yii2.0预定义的别名功能小结
2016/07/04 PHP
Flash+XML滚动新闻代码 无图片 附源码下载
2007/11/22 Javascript
js简单实现删除记录时的提示效果
2013/12/05 Javascript
jQuery删除节点的三个方法即remove()detach()和empty()
2013/12/27 Javascript
jquery ajax 局部刷新小案例
2014/02/08 Javascript
聊一聊JS中this的指向问题
2016/06/17 Javascript
用nodejs搭建websocket服务器
2017/01/23 NodeJs
JavaScript实现翻页功能(附效果图)
2017/02/16 Javascript
JavaScript该如何学习 怎样轻松学习JavaScript
2017/06/12 Javascript
seajs实现强制刷新本地缓存的方法分析
2017/10/16 Javascript
vue2.0 + element UI 中 el-table 数据导出Excel的方法
2018/03/02 Javascript
js操作table中tr的顺序实现上移下移一行的效果
2018/11/22 Javascript
JavaScript设计模式之观察者模式实例详解
2019/01/16 Javascript
如何自定义微信小程序tabbar上边框的颜色
2019/07/09 Javascript
jQuery事件模型默认行为执行顺序及trigger()与 triggerHandler()比较实例分析
2020/04/30 jQuery
Node快速切换版本、版本回退(降级)、版本更新(升级)
2021/01/07 Javascript
[01:10:48]完美世界DOTA2联赛PWL S2 GXR vs PXG 第一场 11.18
2020/11/18 DOTA
Django2.1.3 中间件使用详解
2018/11/26 Python
浅谈PyTorch的可重复性问题(如何使实验结果可复现)
2020/02/20 Python
python GUI库图形界面开发之PyQt5动态加载QSS样式文件
2020/02/25 Python
基于django 的orm中非主键自增的实现方式
2020/05/18 Python
Vans(范斯)德国官网:美国南加州的原创极限运动潮牌
2017/05/02 全球购物
古驰英国官网:GUCCI英国
2020/03/07 全球购物
serialVersionUID具有什么样的特征
2014/02/20 面试题
智乐游戏测试笔试题
2014/05/21 面试题
大学生入党思想汇报
2014/01/14 职场文书
总经理任命书
2014/03/29 职场文书
2014年班主任自我评价范文
2014/04/23 职场文书
暑期社会实践先进个人主要事迹
2014/05/22 职场文书
微笑面对生活演讲稿
2014/09/23 职场文书
财务会计求职信范文
2015/03/20 职场文书
解决MySQL存储时间出现不一致的问题
2021/04/28 MySQL
新手入门Mysql--sql执行过程
2021/06/20 MySQL