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提交并解析后台返回的XML的代码
Nov 03 Javascript
jquery实现非叠加式的搜索框提示效果
Jan 07 Javascript
JavaScript中的typeof操作符用法实例
Apr 05 Javascript
jquery实现华丽的可折角广告代码
Sep 02 Javascript
跟我学习javascript的闭包
Nov 16 Javascript
js实现select选择框效果及美化
Aug 19 Javascript
整理关于Bootstrap表单的慕课笔记
Mar 29 Javascript
vue2.0实现倒计时的插件(时间戳 刷新 跳转 都不影响)
Mar 30 Javascript
Vue SPA单页应用首屏优化实践
Jun 28 Javascript
angularJS1 url中携带参数的获取方法
Oct 09 Javascript
使用JS来动态操作css的几种方法
Dec 18 Javascript
node+multer实现图片上传的示例代码
Feb 18 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中,文件上传
2006/12/06 PHP
php array的学习笔记
2012/05/16 PHP
PHP面向对象程序设计之类常量用法实例
2014/08/20 PHP
php中使用sftp教程
2015/03/30 PHP
PHP实现简单爬虫的方法
2015/07/29 PHP
PHP实现链式操作的三种方法详解
2017/11/16 PHP
PHP单例模式实例分析【防继承,防克隆操作】
2019/05/22 PHP
javascript 面向对象 function类
2010/05/13 Javascript
jquery实现居中弹出层代码
2010/08/25 Javascript
javascript学习(二)javascript常见问题总结
2013/01/02 Javascript
Android中的jQuery:AQuery简介
2014/05/06 Javascript
百度判断手机终端并自动跳转js代码及使用实例
2014/06/11 Javascript
轻松创建nodejs服务器(10):处理POST请求
2014/12/18 NodeJs
node.js读取文件到字符串的方法
2015/06/29 Javascript
js实现简单计算器
2015/11/22 Javascript
实例详解jQuery Mockjax 插件模拟 Ajax 请求
2016/01/12 Javascript
JavaScript设计模式开发中组合模式的使用教程
2016/05/18 Javascript
ajax与json 获取数据并在前台使用简单实例
2017/01/19 Javascript
create-react-app 修改为多入口编译的方法
2018/08/01 Javascript
微信小程序rich-text富文本用法实例分析
2019/05/20 Javascript
python检测IP地址变化并触发事件
2018/12/26 Python
python图片二值化提高识别率代码实例
2019/08/24 Python
决策树剪枝算法的python实现方法详解
2019/09/18 Python
解决django-xadmin列表页filter关联对象搜索问题
2019/11/15 Python
Python实现查找数据库最接近的数据
2020/06/08 Python
给民警的表扬信
2014/01/08 职场文书
环境科学专业优秀毕业生自荐书
2014/02/03 职场文书
开业庆典主持词
2014/03/21 职场文书
协议书模板
2014/04/23 职场文书
项目负责人任命书
2014/06/04 职场文书
销售辞职信范文
2015/03/02 职场文书
公司市场部岗位职责
2015/04/15 职场文书
教师病假条范文
2015/08/17 职场文书
2016重阳节红领巾广播稿
2015/12/18 职场文书
浅谈什么是SpringBoot异常处理自动配置的原理
2021/06/21 Java/Android
JavaScript实现外溢动态爱心的效果的示例代码
2022/03/21 Javascript