手动实现vue2.0的双向数据绑定原理详解


Posted in Vue.js onFebruary 06, 2021

一句话概括:数据劫持(Object.defineProperty)+发布订阅模式

双向数据绑定有三大核心模块(dep 、observer、watcher),它们之间是怎么连接的,下面来一一介绍。

为了大家更好的理解双向数据绑定原理以及它们之间是如何实现关联的,先带领大家复习一下发布订阅模式。

一.首先了解什么是发布订阅模式

直接上代码:

一个简单的发布订阅模式,帮助大家更好的理解双向数据绑定原理

//发布订阅模式
function Dep() {
  this.subs = []//收集依赖(也就是手机watcher实例),
}
Dep.prototype.addSub = function (sub) { //添加订阅者
  this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.notify = function (sub) { //发布,这个方法的作用是遍历数组,让每个订阅者的update方法去执行
  this.subs.forEach((sub) => sub.update())
}

function Watcher(fn) {
  this.fn = fn;
}
Watcher.prototype.update = function () { //添加一个update属性让每一个实例都可以继承这个方法
  this.fn();
}
let watcher = new Watcher(function () {
  alert(1)
});//订阅
let dep = new Dep();
dep.addSub(watcher);//添加依赖,添加订阅者
dep.notify();//发布,让每个订阅者的update方法执行

二.new Vue()的时候做了什么?

只是针对双向数据绑定做说明

<template>
   <div id="app">
    <div>obj.text的值:{{obj.text}}</div>
    <p>word的值:{{word}}</p>
    <input type="text" v-model="word">
  </div>
</template>
<script>
  new Vue({
    el: "#app",
    data: {
      obj: {
        text: "向上",
      },
      word: "学习"
    },
    methods:{
    // ...
    }
  })
</script>

Vue构造函数都干什么了?

function Vue(options = {}) {
  this.$options = options;//接收参数
  var data = this._data = this.$options.data;
  observer(data);//对data中的数据进型循环递归绑定
  for (let key in data) {
    let val = data[key];
    observer(val);
    Object.defineProperty(this, key, {
      enumerable: true,
      get() {
        return this._data[key];
      },
      set(newVal) {
        this._data[key] = newVal;
      }
    })
  }
  new Compile(options.el, this)
};

在new Vue({…})构造函数时,首先获取参数options,然后把参数中的data数据赋值给当前实例的_data属性上(this._data = this.$options.data),重点来了,那下面的遍历是为什么呢?首先我们在操作数据的时候是this.word获取,而不是this._data.word,所以是做了一个映射,在获取数据的时候this.word,其实是获取的this._data.word的值,大家可以在自己项目中输出this查看一下

手动实现vue2.0的双向数据绑定原理详解

1.接下来看看observer方法干了什么

function observer(data) {
  if (typeof data !== "object") return;
  return new Observer(data);//返回一个实例
}
function Observer(data) {
  let dep = new Dep();//创建一个dep实例
  for (let key in data) {//对数据进行循环递归绑定
    let val = data[key];
    observer(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      get() {
        Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一个实例
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        observer(newVal);
        dep.notify() //让所有方法执行
      }
    })
  }
}

Observer构造函数,首先let dep=new Dep(),作为之后的触发数据劫持的get方法和set方法时,去收集依赖和发布时调用,主要的操作就是通过Object.defineProperty对data数据进行循环递归绑定,使用getter/setter修改其默认读写,用于收集依赖和发布更新。

2.再来看看Compile具体干了那些事情

function Compile(el, vm) {
  vm.$el = document.querySelector(el);
  let fragment = document.createDocumentFragment(); //创建文档碎片,是object类型
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child);
  };//用while循环把所有节点都添加到文档碎片中,之后都是对文档碎片的操作,最后再把文档碎片添加到页面中,这里有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到fragment中时,会删除原来的节点。
  replace(fragment);

  function replace(fragment) {
    Array.from(fragment.childNodes).forEach((node) => {//循环所有的节点
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/;
      if (node.nodeType === 3 && reg.test(text)) {//判断当前节点是不是文本节点且符不符合{{obj.text}}的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher)
        console.log(RegExp.$1); //obj.text
        let arr = RegExp.$1.split("."); //转换成数组的方式[obj,text],方便取值
        let val = vm;
        arr.forEach((key) => { //实现取值this.obj.text
          val = val[key];
        });
        new Watcher(vm, RegExp.$1, function (newVal) {
          node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
        });
        node.textContent = text.replace(/\{\{(.*)\}\}/, val); //对节点内容进行初始化的赋值
      }
      if (node.nodeType === 1) { //说明是元素节点
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((item) => {
          if (item.name.indexOf("v-") >= 0) {//判断是不是v-model这种指令
            node.value = vm[item.value]//对节点赋值操作
          }
          //添加订阅者
          new Watcher(vm, item.value, function (newVal) {
            node.value = vm[item.value]
          });
          node.addEventListener("input", function (e) {
            let newVal = e.target.value;
            vm[item.value] = newVal;
          })
        })
      }
      if (node.childNodes) { //这个节点里还有子元素,再递归
        replace(node);
      }
    })
  }

  //这是页面中的文档已经没有了,所以还要把文档碎片放到页面中
  vm.$el.appendChild(fragment);

}

Compile(编译方法)

首先解释一下DocuemntFragment(文档碎片)它是一个dom节点收容器,当你创造了多个节点,当每个节点都插入到文档当中都会引发一次回流,也就是说浏览器要回流多次,十分耗性能,而使用文档碎片就是把多个节点都先放入到一个容器中,最后再把整个容器直接插入就可以了,浏览器只回流了1次。

Compile方法首先遍历文档碎片的所有节点,1.判断是否是文本节点且符不符合{{obj.text}}的双大括号的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) 2.判断是否是元素节点且属性中是否含有v-model这种指令,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) ,直至遍历完成。

最后别忘了把文档碎片放到页面中

3.Dep构造函数(怎么收集依赖的)

var uid=0;
//发布订阅
function Dep() {
  this.id=uid++;
  this.subs = [];
}
Dep.prototype.addSub = function (sub) { //订阅
  this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.depend = function () { // 订阅管理器
  if(Dep.target){//只有Dep.target存在时采取添加
    Dep.target.addDep(this);
  }
}
Dep.prototype.notify = function (sub) { //发布,遍历数组让每个订阅者的update方法去执行
  this.subs.forEach((sub) => sub.update())
}

Dep构造函数内部有一个id和一个subs,id=uid++ ,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。depend方法就是一个订阅的管理器,会调用当前watcher的addDep方法添加订阅者,当触发数据劫持(Object.defineProperty)的get方法时会调用Dep.target && dep.depend(Dep.target)添加订阅者,当数据改变时触发数据劫持(Object.defineProperty)的set方法时会调用dep.notify方法更新操作。

4.Watcher构造函数干了什么

function Watcher(vm, exp, fn) {
  this.fn = fn;
  this.vm = vm;
  this.exp = exp //
  this.newDeps = [];
  this.depIds = new Set();
  this.newDepIds = new Set();
  Dep.target = this; //this是指向当前(Watcher)的一个实例
  let val = vm;
  let arr = exp.split(".");
  arr.forEach((k) => { //取值this.obj.text
    val = val[k] //取值this.obj.text,就会触发数据劫持的get方法,把当前的订阅者(watcher实例)添加到依赖中
  });
  Dep.target = null;
}
Watcher.prototype.addDep = function (dep) {
  var id=dep.id;
  if(!this.newDepIds.has(id)){
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if(!this.depIds.has(id)){
      dep.addSub(this);
    }
  }
 
}
Watcher.prototype.update = function () { //这就是每个绑定的方法都添加一个update属性
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach((k) => { 
    val = val[k] //取值this.obj.text,传给fn更新操作
  });
  this.fn(val); //传一个新值
}

Watcher构造函数干了什么

1 接收参数,定义了几个私有属性( this.newDep ,this.depIds
,this.newDepIds)

2. Dep.target = this,通过参数进行data取值操作,这就会触发Object.defineProperty的get方法,它会通过订阅者管理器(dep.depend())添加订阅者,添加完之后再将Dep.target=null置为空;

3.原型上的addDep是通过id这个唯一标识,和几个私有属性的判断防止订阅者被多次重复添加

4.update方法就是当数据更新时,dep.notify()执行,触发订阅者的update这个方法, 执行发布更新操作。

总结一下

vue2.0中双向数据绑定,简单来说就是Observer、Watcher、Dep三大部分;

1.首先用Object.defineProperty()循环递归实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;

2.在编译的时候,创建文档碎片,把所有节点添加到文档碎片中,遍历文档碎片的所有结点,如果是{{}},v-model这种,new Watcher()实例并向dep的subs数组中添加该实例

3.最后修改值就会触发Object.defineProperty()的set方法,在set方法中会执行dep.notify(),然后循环调用所有订阅者的update方法更新视图。

到此这篇关于手动实现vue2.0的双向数据绑定原理的文章就介绍到这了,更多相关vue2.0双向数据绑定内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
element-plus一个vue3.xUI框架(element-ui的3.x 版初体验)
Dec 02 Vue.js
Vue与React的区别和优势对比
Dec 18 Vue.js
Vue中computed和watch有哪些区别
Dec 19 Vue.js
基于vuex实现购物车功能
Jan 10 Vue.js
vue 中 get / delete 传递数组参数方法
Mar 23 Vue.js
详解vue中v-for的key唯一性
May 15 Vue.js
vue-element-admin项目导入和导出的实现
May 21 Vue.js
springboot+VUE实现登录注册
May 27 Vue.js
解决vue $http的get和post请求跨域问题
Jun 07 Vue.js
vue实现移动端div拖动效果
Mar 03 Vue.js
vue修饰符.capture和.self的区别
Apr 22 Vue.js
Vue 打包后相对路径的引用问题
Jun 05 Vue.js
vue3.0 自适应不同分辨率电脑的操作
Feb 06 #Vue.js
vue使用echarts画组织结构图
Feb 06 #Vue.js
vue 根据选择的月份动态展示日期对应的星期几
Feb 06 #Vue.js
解决vue项目本地启动时无法携带cookie的问题
Feb 06 #Vue.js
如何封装Vue Element的table表格组件
Feb 06 #Vue.js
Vue实现圆环进度条的示例
Feb 06 #Vue.js
vue浏览器返回监听的具体步骤
Feb 03 #Vue.js
You might like
smtp邮件发送一例
2006/10/09 PHP
在PHP中读取和写入WORD文档的代码
2008/04/09 PHP
php注销代码(session注销)
2012/05/31 PHP
php中的ini配置原理详解
2014/10/14 PHP
CI配置多数据库访问的方法
2016/03/28 PHP
php图像处理函数imagecopyresampled用法详解
2016/12/02 PHP
浅谈thinkphp5 instance 的简单实现
2017/07/30 PHP
thinkphp5+layui实现的分页样式示例
2019/10/08 PHP
laravel csrf排除路由,禁止,关闭指定路由的例子
2019/10/21 PHP
jquery 图片预加载 自动等比例缩放插件
2008/12/25 Javascript
JS 实现获取打开一个界面中输入的值
2013/03/19 Javascript
用html+css+js实现的一个简单的图片切换特效
2014/05/28 Javascript
简述JavaScript的正则表达式中test()方法的使用
2015/06/16 Javascript
jQuery Validate插件实现表单强大的验证功能
2015/12/18 Javascript
无需 Flash 使用 jQuery 复制文字到剪贴板
2016/04/26 Javascript
Vue开发中整合axios的文件整理
2017/04/29 Javascript
JS图片预加载插件详解
2017/06/21 Javascript
switchery按钮的使用方法
2017/12/18 Javascript
详解React开发必不可少的eslint配置
2018/02/05 Javascript
详解vuex之store拆分即多模块状态管理(modules)篇
2018/11/13 Javascript
小程序双头slider选择器的实现示例
2020/03/31 Javascript
vue中实现点击变成全屏的多种方法
2020/09/27 Javascript
[01:04:35]2018DOTA2亚洲邀请赛 4.3 突围赛 Secret vs VG 第一场
2018/04/04 DOTA
python中的yield使用方法
2014/02/11 Python
使用Python中的tkinter模块作图的方法
2017/02/07 Python
深入学习Python中的上下文管理器与else块
2017/08/27 Python
Python实现数据结构线性链表(单链表)算法示例
2019/05/04 Python
Windows下实现将Pascal VOC转化为TFRecords
2020/02/17 Python
Python实现疫情通定时自动填写功能(附代码)
2020/05/27 Python
python 实现非极大值抑制算法(Non-maximum suppression, NMS)
2020/10/15 Python
CSS3 分类菜单效果
2019/05/27 HTML / CSS
工商企业管理应届生求职信
2013/11/03 职场文书
商务主管岗位职责
2013/12/08 职场文书
培训自我鉴定
2014/01/31 职场文书
地球一小时宣传标语
2014/06/24 职场文书
房地产公司财务总监岗位职责
2015/04/03 职场文书