vue 数据双向绑定的实现方法


Posted in Vue.js onMarch 04, 2021

1. 前言

本文适合于学习Vue源码的初级学者,阅读后,你将对Vue的数据双向绑定原理有一个大致的了解,认识Observer、Compile、Wathcer三大角色(如下图所示)以及它们所发挥的功能。

本文将一步步带你实现简易版的数据双向绑定,每一步都会详细分析这一步要解决的问题以及代码为何如此写,因此,在阅读完本文后,希望你能自己动手实现一个简易版数据双向绑定。

vue 数据双向绑定的实现方法

2. 代码实现

2.1 目的分析

本文要实现的效果如下图所示:

vue 数据双向绑定的实现方法

本文用到的HTML和JS主体代码如下:

<div id="app">
  <h1 v-text="msg"></h1>
  <input type="text" v-model="msg">
  <div>
    <h1 v-text="msg2"></h1>
    <input type="text" v-model="msg2">
  </div>
</div>
let vm = new Vue({
    el: "#app",
    data: {
      msg: "hello world",
      msg2: "hello xiaofei"
    }
  })

我们将按照下面三个步骤来实现:

  • 第一步:将data中的数据同步到页面上,实现 M ==> V 的初始化;
  • 第二步:当input框中输入值时,将新值同步到data中,实现 V ==> M 的绑定;
  • 第三步:当data数据发生更新的时候,触发页面发生变化,实现 M ==> V 的绑定。

2.2 实现过程

2.2.1 入口代码

首先,我们要创造一个Vue类,这个类接收一个 options 对象,同时,我们要对 options 对象中的有效信息进行保存;

然后,我们有三个主要模块:Observer、Compile、Wathcer,其中,Observer用来数据劫持的,Compile用来解析元素,Wathcer是观察者。可以写出如下代码:(Observer、Compile、Wathcer这三个概念,不用细究,后面会详解讲解)。

class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 保存有效信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]},用来存放每个属性观察者
      this.$watcher = {};

      // 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile() {}
    observe() {}
  }

2.2.2 页面初始化

在这一步,我们要实现页面的初始化,即解析出v-text和v-model指令,并将data中的数据渲染到页面中。

这一步的关键在于实现compile方法,那么该如何解析el元素呢?思路如下:

  • 首先要获取到el下面的所有子节点,然后遍历这些子节点,如果子节点还有子节点,那我们就需要用到递归的思想;
  • 遍历子节点找到所有有指令的元素,并将对应的数据渲染到页面中。

代码如下:(主要看compile那部分)

class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 获取有用信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]}
      this.$watcher = {};

      // 2. 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 3. 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile(el) {
      // 解析元素下的每一个子节点, 所以要获取el.children
      // 备注: children 返回元素集合, childNodes返回节点集合
      let nodes = el.children;

      // 解析每个子节点的指令
      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        // 如果当前节点还有子元素, 递归解析该节点
        if(node.children){
          this.compile(node);
        }
        // 解析带有v-text指令的元素
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          node.textContent = this.$data[attrVal]; // 渲染页面
        }
        // 解析带有v-model指令的元素
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];
        }
      }
    }
    observe(data) {}
  }

这样,我们就实现页面的初始化了。

vue 数据双向绑定的实现方法

2.2.3 视图影响数据

因为input带有v-model指令,因此我们要实现这样一个功能:在input框中输入字符,data中绑定的数据发生相应的改变。

我们可以在input这个元素上绑定一个input事件,事件的效果就是:将data中的相应数据修改为input中的值。

这一部分的实现代码比较简单,只要看标注那个地方就明白了,代码如下:

class Vue {
    constructor(options) {
      this.$el = document.querySelector(options.el);
      this.$data = options.data;
      
      this.$watcher = {};  

      this.compile(this.$el);

      this.observe(this.$data);
    }
    compile(el) {
      let nodes = el.children;

      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        if(node.children){
          this.compile(node);
        }
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          node.textContent = this.$data[attrVal];
        }
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];
          // 看这里!!只多了三行代码!!
          node.addEventListener("input", (ev)=>{
            this.$data[attrVal] = ev.target.value;
            // 可以试着在这里执行:console.log(this.$data),
            // 就可以看到每次在输入框输入文字的时候,data中的msg值也发生了变化
          })
        }
      }
    }
    observe(data) {}
  }

2.2.4 数据影响视图

至此,我们已经实现了:当我们在input框中输入字符的时候,data中的数据会自动发生更新;

本小节的主要任务是:当data中的数据发生更新的时候,绑定了该数据的元素会在页面上自动更新视图。具体思路如下:

1) 我们将要实现一个 Wathcer 类,它有一个update方法,用来更新页面。观察者的代码如下:

class Watcher{
    constructor(node, updatedAttr, vm, expression){
      // 将传进来的值保存起来,这些数据都是渲染页面时要用到的数据
      this.node = node;
      this.updatedAttr = updatedAttr;
      this.vm = vm;
      this.expression = expression;
      this.update();
    }
    update(){
      this.node[this.updatedAttr] = this.vm.$data[this.expression];
    }
  }

2) 试想,我们该给哪些数据添加观察者?何时给数据添加观察者?

在解析元素的时候,当解析到v-text和v-model指令的时候,说明这个元素是需要和数据双向绑定的,因此我们在这时往容器中添加观察者。我们需用到这样一个数据结构:{属性1: [wathcer1, wathcer2...], 属性2: [...]},如果不是很清晰,可以看下图:

vue 数据双向绑定的实现方法

可以看到:vue实例中有一个$wathcer对象,$wathcer的每个属性对应每个需要绑定的数据,值是一个数组,用来存放观察了该数据的观察者。(备注:Vue源码中专门创造了Dep这么一个类,对应这里所说的数组,本文属于简易版本,就不过多介绍了)

3) 劫持数据:利用对象的访问器属性getter和setter做到当数据更新的时候,触发一个动作,这个动作的主要目的就是让所有观察了该数据的观察者执行update方法。

总结一下,在本小节我们需要做的工作:

  1. 实现一个Wathcer类;
  2. 在解析指令的时候(即在compile方法中)添加观察者;
  3. 实现数据劫持(实现observe方法)。

完整代码如下:

class Vue {
    // 接收传进来的对象
    constructor(options) {
      // 获取有用信息
      this.$el = document.querySelector(options.el);
      this.$data = options.data;

      // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]}
      this.$watcher = {};

      // 解析元素: 实现Compile
      this.compile(this.$el); // 要解析元素, 就得把元素传进去

      // 劫持数据: 实现 Observer
      this.observe(this.$data); // 要劫持数据, 就得把数据传入
    }
    compile(el) {
      // 解析元素下的每一个子节点, 所以要获取el.children
      // 拓展: children 返回元素集合, childNodes返回节点集合
      let nodes = el.children;

      // 解析每个子节点的指令
      for (var i = 0, length = nodes.length; i < length; i++) {
        let node = nodes[i];
        // 如果当前节点还有子元素, 递归解析该节点
        if (node.children) {
          this.compile(node);
        }
        if (node.hasAttribute("v-text")) {
          let attrVal = node.getAttribute("v-text");
          // node.textContent = this.$data[attrVal]; 
          // Watcher在实例化时调用update, 替代了这行代码

          /**
           * 试想Wathcer要更新节点数据的时候要用到哪些数据? 
           * e.g.   p.innerHTML = vm.$data[msg]
           * 所以要传入的参数依次是: 当前节点node, 需要更新的节点属性, vue实例, 绑定的数据属性
          */
          // 往容器中添加观察者: {msg1: [Watcher, Watcher...], msg2: [...]}
          if (!this.$watcher[attrVal]) {
            this.$watcher[attrVal] = [];
          }
          this.$watcher[attrVal].push(new Watcher(node, "innerHTML", this, attrVal))
        }
        if (node.hasAttribute("v-model")) {
          let attrVal = node.getAttribute("v-model");
          node.value = this.$data[attrVal];

          node.addEventListener("input", (ev) => {
            this.$data[attrVal] = ev.target.value;
          })

          if (!this.$watcher[attrVal]) {
            this.$watcher[attrVal] = [];
          }
          // 不同于上处用的innerHTML, 这里input用的是vaule属性
          this.$watcher[attrVal].push(new Watcher(node, "value", this, attrVal))
        }
      }
    }
    observe(data) {
      Object.keys(data).forEach((key) => {
        let val = data[key];  // 这个val将一直保存在内存中,每次访问data[key],都是在访问这个val
        Object.defineProperty(data, key, {
          get() {
            return val;  // 这里不能直接返回data[key],不然会陷入无限死循环
          },
          set(newVal) {
            if (val !== newVal) {
              val = newVal;// 同理,这里不能直接对data[key]进行设置,会陷入死循环
              this.$watcher[key].forEach((w) => {
                w.update();
              })
            }
          }
        })
      })
    }
  }

  class Watcher {
    constructor(node, updatedAttr, vm, expression) {
      // 将传进来的值保存起来
      this.node = node;
      this.updatedAttr = updatedAttr;
      this.vm = vm;
      this.expression = expression;
      this.update();
    }
    update() {
      this.node[this.updatedAttr] = this.vm.$data[this.expression];
    }
  }

  let vm = new Vue({
    el: "#app",
    data: {
      msg: "hello world",
      msg2: "hello xiaofei"
    }
  })

至此,代码就完成了。

3. 未来的计划

用设计模式的知识,分析上面这份源码存在的问题,并和Vue源码进行比对,算是对Vue源码的解析

以上就是vue 数据双向绑定的实现方法的详细内容,更多关于vue 数据双向绑定的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
如何使用 vue-cli 创建模板项目
Nov 19 Vue.js
在Vue中使用mockjs代码实例
Nov 25 Vue.js
Vue中computed和watch有哪些区别
Dec 19 Vue.js
Vue实现随机验证码功能
Dec 29 Vue.js
vue调用微信JSDK 扫一扫,相册等需要注意的事项
Jan 03 Vue.js
vue3自定义dialog、modal组件的方法
Jan 04 Vue.js
详解template标签用法(含vue中的用法总结)
Jan 12 Vue.js
vue监听键盘事件的相关总结
Jan 29 Vue.js
基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能
Feb 23 Vue.js
vue路由实现登录拦截
Mar 24 Vue.js
开发一个封装iframe的vue组件
Mar 29 Vue.js
vue实现水波涟漪效果的点击反馈指令
May 31 Vue.js
vue3.0中使用element的完整步骤
Mar 04 #Vue.js
VUE实现吸底按钮
Mar 04 #Vue.js
vue实现可移动的悬浮按钮
Mar 04 #Vue.js
vue中axios封装使用的完整教程
Mar 03 #Vue.js
详解Vue.js 可拖放文本框组件的使用
Mar 03 #Vue.js
详解vue3中组件的非兼容变更
Mar 03 #Vue.js
vite2.0+vue3移动端项目实战详解
Mar 03 #Vue.js
You might like
回首过去10年中最搞笑的10部动漫,哪一部让你节操尽碎?
2020/03/03 日漫
域名查询代码公布
2006/10/09 PHP
杏林同学录(五)
2006/10/09 PHP
如何在PHP中进行身份认证
2006/10/09 PHP
PHP Parse Error: syntax error, unexpected $end 错误的解决办法
2012/06/05 PHP
用PHP提取中英文词语以及数字的首字母的方法介绍
2013/04/23 PHP
浅析Yii2中GridView常见操作
2016/04/22 PHP
jQuery 对象中的类数组操作
2009/04/27 Javascript
DOM 脚本编程中的兄弟节点
2009/10/31 Javascript
FileUpload上传图片(图片不变形)
2010/08/05 Javascript
用js正确判断用户名cookie是否存在的方法
2014/01/28 Javascript
基于jQuery通过jQuery.form.js插件使用ajax提交form表单
2015/08/17 Javascript
js实现仿qq消息的弹出窗效果
2016/01/06 Javascript
JavaScript黑洞数字之运算路线查找算法(递归算法)实例
2016/01/28 Javascript
bootstrap table小案例
2016/10/21 Javascript
JavaScript交换两个变量值的七种解决方案
2016/12/01 Javascript
angularJS利用ng-repeat遍历二维数组的实例代码
2017/06/03 Javascript
微信小程序访问豆瓣电影api的实现方法
2019/03/31 Javascript
angularjs请求数据的方法示例
2019/08/06 Javascript
微信小程序全局变量GLOBALDATA的定义和调用过程解析
2019/09/23 Javascript
vue 限制input只能输入正数的操作
2020/08/05 Javascript
[02:02:38]VG vs Mineski Supermajor 败者组 BO3 第一场 6.6
2018/06/07 DOTA
[32:56]完美世界DOTA2联赛PWL S3 Rebirth vs CPG 第二场 12.11
2020/12/16 DOTA
python使用Pycharm创建一个Django项目
2018/03/05 Python
Python可变和不可变、类的私有属性实例分析
2019/05/31 Python
pyqt 实现在Widgets中显示图片和文字的方法
2019/06/13 Python
Python统计时间内的并发数代码实例
2019/12/28 Python
在主流系统之上安装Pygame的方法
2020/05/20 Python
如何利用Python给自己的头像加一个小国旗(小月饼)
2020/10/02 Python
英国的屈臣氏:Boots博姿
2017/12/23 全球购物
DataReader和DataSet的异同
2014/12/31 面试题
出纳岗位职责
2013/11/09 职场文书
财务会计专业推荐信
2013/11/30 职场文书
博士学位自我鉴定范文
2013/12/26 职场文书
计划生育汇报材料
2014/12/26 职场文书
Redis中缓存穿透/击穿/雪崩问题和解决方法
2021/12/04 Redis