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 相关文章推荐
JavaScript中关于indexOf的使用方法与问题小结
Aug 05 Javascript
js常用排序实现代码
Dec 28 Javascript
javascript中xml操作实现代码
Nov 21 Javascript
基于JS2Image实现圣诞树代码
Dec 24 Javascript
详解Javascript事件驱动编程
Jan 03 Javascript
基于css3新属性transform及原生js实现鼠标拖动3d立方体旋转
Jun 12 Javascript
node.js文件上传处理示例
Oct 27 Javascript
JS实现焦点图轮播效果的方法详解
Dec 19 Javascript
Vue实现一个无限加载列表功能
Nov 13 Javascript
JavaScript创建防篡改对象的方法分析
Dec 30 Javascript
js的各种数据类型判断的介绍
Jan 19 Javascript
详解webpack4.x之搭建前端开发环境
Mar 28 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 调试工具Debug Tools
2011/04/30 PHP
php为字符串前后添加指定数量字符的方法
2015/05/04 PHP
Smarty模板语法详解
2019/07/20 PHP
基于jQuery的弹出消息插件 DivAlert之旅(一)
2010/04/01 Javascript
Prototype源码浅析 Enumerable部分之each方法
2012/01/16 Javascript
JSONP 跨域访问代理API-yahooapis实现代码
2012/12/02 Javascript
JS实现进入页面时渐变背景色的方法
2015/02/25 Javascript
js生成随机数(指定范围)的实例代码
2016/07/10 Javascript
JavaScript自定义函数实现查找两个字符串最长公共子串的方法
2016/11/24 Javascript
微信小程序自定义对话框弹出和隐藏动画
2018/07/19 Javascript
es6数值的扩展方法
2019/03/11 Javascript
[01:18:31]DOTA2-DPC中国联赛定级赛 LBZS vs Magma BO3第一场 1月10日
2021/03/11 DOTA
在Python中使用Mako模版库的简单教程
2015/04/08 Python
详解使用python crontab设置linux定时任务
2016/12/08 Python
用python实现将数组元素按从小到大的顺序排列方法
2018/07/02 Python
Python数据结构之栈、队列及二叉树定义与用法浅析
2018/12/27 Python
python实现计数排序与桶排序实例代码
2019/03/28 Python
详解Python:面向对象编程
2019/04/10 Python
图文详解python安装Scrapy框架步骤
2019/05/20 Python
python粘包问题及socket套接字编程详解
2019/06/29 Python
python 使用pygame工具包实现贪吃蛇游戏(多彩版)
2019/10/30 Python
python实现引用其他路径包里面的模块
2020/03/09 Python
CSS中越界问题的经典解决方案【推荐】
2016/04/19 HTML / CSS
世界最大的私人旅行指南出版商:孤独星球
2016/08/23 全球购物
澳大利亚在线消费电子产品商店:TobyDeals
2020/01/05 全球购物
英国发展最快的在线超市之一:Click Marketplace
2021/02/15 全球购物
物业管理员岗位职责范文
2013/11/25 职场文书
销售主管的自我评价分享
2014/01/03 职场文书
大学生职业生涯规划范文
2014/01/08 职场文书
公司外出活动方案
2014/08/14 职场文书
关于运动会的广播稿
2014/09/22 职场文书
党员自我剖析材料范文
2014/10/06 职场文书
党支部对转正的意见
2015/06/02 职场文书
Python+OpenCV实现在图像上绘制矩形
2022/03/21 Python
【海涛教你打DOTA】死灵飞龙第一视角解说
2022/04/01 DOTA
Windows10安装Apache2.4的方法步骤
2022/06/25 Servers