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+element-ui添加自定义右键菜单的方法示例
Dec 08 Vue.js
Vue组件简易模拟实现购物车
Dec 21 Vue.js
vue+vant 上传图片需要注意的地方
Jan 03 Vue.js
如何在VUE中使用vue-awesome-swiper
Jan 04 Vue.js
vue.js watch经常失效的场景与解决方案
Jan 07 Vue.js
Vue实现图书管理案例
Jan 20 Vue.js
vue登录页实现使用cookie记住7天密码功能的方法
Feb 18 Vue.js
简单聊聊Vue中的计算属性和属性侦听
Oct 05 Vue.js
vue ref如何获取子组件属性值
Mar 31 Vue.js
教你部署vue项目到docker
Apr 05 Vue.js
vue 把二维或多维数组转一维数组
Apr 24 Vue.js
关于vue-router-link选择样式设置
Apr 30 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数据库调用类调用实例(详细注释)
2012/07/12 PHP
PHP模糊查询技术实例分析【附源码下载】
2019/03/07 PHP
复制小说文本时出现的随机乱码的去除方法
2010/09/07 Javascript
jQuery回车实现登录简单实现
2013/08/20 Javascript
JS下拉缓冲菜单示例代码
2013/08/30 Javascript
使用jQuery监听DOM元素大小变化
2016/02/24 Javascript
基于slideout.js实现移动端侧边栏滑动特效
2016/11/28 Javascript
EasyUI学习之DataGird分页显示数据
2016/12/29 Javascript
elemetUi 组件--el-upload实现上传Excel文件的实例
2017/10/27 Javascript
使用Vue开发一个实时性时间转换指令
2018/01/17 Javascript
Vue单页及多页应用全局配置404页面实践记录
2018/05/22 Javascript
浅析vue.js数组的变异方法
2018/06/30 Javascript
微信小程序实现自定义picker选择器弹窗内容
2020/05/26 Javascript
详解vue使用$http服务端收不到参数
2019/04/19 Javascript
LayUI switch 开关监听 获取属性值、更改状态的方法
2019/09/21 Javascript
基于JavaScript实现表格隔行换色
2020/05/08 Javascript
jQuery+css实现的点击图片放大缩小预览功能示例【图片预览 查看大图】
2020/05/29 jQuery
手写Vue源码之数据劫持示例详解
2021/01/04 Vue.js
Python实现遍历目录的方法【测试可用】
2017/03/22 Python
Python urls.py的三种配置写法实例详解
2017/04/28 Python
itchat和matplotlib的结合使用爬取微信信息的实例
2017/08/25 Python
TensorFlow实现非线性支持向量机的实现方法
2018/04/28 Python
对Python 简单串口收发GUI界面的实例详解
2019/06/12 Python
Django之模板层的实现代码
2019/09/09 Python
Python中低维数组填充高维数组的实现
2019/12/02 Python
python序列化与数据持久化实例详解
2019/12/20 Python
Python通过Pillow实现图片对比
2020/04/29 Python
python高级特性简介
2020/08/13 Python
香港中原电器网上商店:Chung Yuen
2019/06/26 全球购物
美国优质马术服装购买网站:Breeches.com
2019/12/16 全球购物
专项法律服务方案
2014/06/11 职场文书
商务英语专业求职信
2014/06/26 职场文书
与美同行演讲稿
2014/09/13 职场文书
《雪域豹影》读后感:父爱的伟大
2019/12/23 职场文书
python实现简单石头剪刀布游戏
2021/10/24 Python
详解Anyscript开发指南绕过typescript类型检查
2022/09/23 Javascript