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使用vant中的checkbox实现全选功能
Nov 17 Vue.js
Vue-router中hash模式与history模式的区别详解
Dec 15 Vue.js
Vue 修改网站图标的方法
Dec 31 Vue.js
手写Vue源码之数据劫持示例详解
Jan 04 Vue.js
vue3中的组件间通信
Mar 31 Vue.js
vue backtop组件的实现完整代码
Apr 07 Vue.js
如何理解Vue简单状态管理之store模式
May 15 Vue.js
解读Vue组件注册方式
May 15 Vue.js
vue-cli4.5.x快速搭建项目
May 30 Vue.js
vue3获取当前路由地址
Feb 18 Vue.js
vue+iview实现手机号分段输入框
Mar 25 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
PHP foreach循环使用详解与实例代码
2010/05/08 PHP
关于JSON以及JSON在PHP中的应用技巧
2013/11/27 PHP
PHP中使用addslashes函数转义的安全性原理分析
2014/11/03 PHP
php封装的验证码工具类完整实例
2016/10/19 PHP
php实现微信企业号支付个人的方法详解
2017/07/26 PHP
BOOM vs RR BO3 第二场2.13
2021/03/10 DOTA
ExtJS扩展 垂直tabLayout实现代码
2009/06/21 Javascript
js实现点击注册按钮开始读秒倒计时的小例子
2013/05/11 Javascript
jQuery实现动态添加和删除一个div
2015/08/12 Javascript
angular源码学习第一篇 setupModuleLoader方法
2016/10/20 Javascript
node.js基于mongodb的搜索分页示例
2017/01/22 Javascript
ES5 ES6中Array对象去除重复项的方法总结
2017/04/27 Javascript
select自定义小三角样式代码(实用总结)
2017/08/18 Javascript
JS库之Particles.js中文开发手册及参数详解
2017/09/13 Javascript
浅谈vue中慎用style的scoped属性
2017/11/28 Javascript
D3.js实现简洁实用的动态仪表盘的示例
2018/04/04 Javascript
jquery引入外部CDN 加载失败则引入本地jq库
2018/05/23 jQuery
使用vue的transition完成滑动过渡的示例代码
2018/06/25 Javascript
搭建一个nodejs脚手架的方法步骤
2019/06/28 NodeJs
详解Vue的watch中的immediate与watch是什么意思
2019/12/30 Javascript
基于redis的小程序登录实现方法流程分析
2020/05/25 Javascript
Django 使用Ajax进行前后台交互的示例讲解
2018/05/28 Python
Python 循环终止语句的三种方法小结
2019/06/24 Python
pycharm配置当鼠标悬停时快速提示方法参数
2019/07/31 Python
flask 使用 flask_apscheduler 做定时循环任务的实现
2019/12/10 Python
Python selenium文件上传下载功能代码实例
2020/04/13 Python
PythonPC客户端自动化实现原理(pywinauto)
2020/05/28 Python
python初步实现word2vec操作
2020/06/09 Python
python怎么判断素数
2020/07/01 Python
django filter过滤器实现显示某个类型指定字段不同值方式
2020/07/16 Python
英语翻译系毕业生求职信
2013/09/29 职场文书
违反课堂纪律检讨书
2014/01/19 职场文书
2014年党员承诺书范文
2014/05/20 职场文书
2014年班长个人工作总结
2014/11/14 职场文书
装饰施工员岗位职责
2015/04/11 职场文书
谢师宴家长致辞
2015/07/27 职场文书