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 相关文章推荐
Prototype ObjectRange对象学习
Jul 19 Javascript
IE之动态添加DOM节点触发window.resize事件
Jul 27 Javascript
zeroclipboard复制到剪切板的flash
Aug 04 Javascript
用jquery实现点击栏目背景色改变
Dec 10 Javascript
jquery中的查找parents与closest方法之间的区别
Dec 02 Javascript
最丑的时钟效果!js canvas时钟制作方法
Aug 15 Javascript
jquery pagination插件动态分页实例(Bootstrap分页)
Dec 23 Javascript
关于使用axios的一些心得技巧分享
Jul 02 Javascript
Node.js 使用递归实现遍历文件夹中所有文件
Sep 18 Javascript
vue-router 手势滑动触发返回功能
Sep 30 Javascript
详解vue中v-bind:style效果的自定义指令
Jan 21 Javascript
JavaScript中的Proxy对象
Nov 27 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的preg_match匹配字符串长度问题解决方法
2014/05/03 PHP
PHP之uniqid()函数用法
2014/11/03 PHP
Yii框架表单模型和验证用法
2016/05/20 PHP
PHP图像处理技术实例总结【绘图、水印、验证码、图像压缩】
2018/12/08 PHP
基于jquery实现的可以编辑选择的下拉框的代码
2010/11/19 Javascript
JS实现div内部的文字或图片自动循环滚动代码
2013/04/19 Javascript
对JavaScript中this指针的新理解分享
2015/01/31 Javascript
JavaScript从数组中删除指定值元素的方法
2015/03/18 Javascript
JavaScript检测弹出窗口是否已经关闭的方法
2015/03/24 Javascript
AngularJS中的过滤器filter用法完全解析
2016/04/22 Javascript
ES7中利用Await减少回调嵌套的方法详解
2017/11/01 Javascript
详解Vue路由钩子及应用场景(小结)
2017/11/07 Javascript
vue进行图片的预加载watch用法实例讲解
2018/02/07 Javascript
vue项目中使用AES实现密码加密解密(ECB和CBC两种模式)
2019/08/12 Javascript
vue elementUI 表单校验的实现代码(多层嵌套)
2019/11/06 Javascript
基于javascript实现贪吃蛇经典小游戏
2020/04/10 Javascript
es6函数name属性功能与用法实例分析
2020/04/18 Javascript
Javascript实现单选框效果
2020/12/09 Javascript
[56:24]DOTA2上海特级锦标赛主赛事日 - 3 胜者组第二轮#1Liquid VS MVP.Phx第二局
2016/03/04 DOTA
python实现数通设备tftp备份配置文件示例
2014/04/02 Python
Python基于select实现的socket服务器
2016/04/13 Python
python3.6连接MySQL和表的创建与删除实例代码
2017/12/28 Python
Ubuntu下使用python读取doc和docx文档的内容方法
2018/05/08 Python
Python元组知识点总结
2019/02/18 Python
VPS CENTOS 上配置python,mysql,nginx,uwsgi,django的方法详解
2019/07/01 Python
Python使用matplotlib 画矩形的三种方式分析
2019/10/31 Python
Pycharm及python安装详细教程(图解)
2020/07/31 Python
python实现暗通道去雾算法的示例
2020/09/27 Python
CSS3 按钮边框动画的实现
2020/11/12 HTML / CSS
关爱老人标语
2014/06/21 职场文书
大学第二课堂活动总结
2014/07/08 职场文书
旷课检讨书范文
2014/10/30 职场文书
个人党性锻炼总结
2015/03/05 职场文书
2015年人民调解工作总结
2015/05/18 职场文书
侵犯商业秘密的律师函
2015/05/27 职场文书
2019年让高校“心动”的自荐信
2019/03/25 职场文书