手动实现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 相关文章推荐
vue+iview分页组件的封装
Nov 17 Vue.js
8个非常实用的Vue自定义指令
Dec 15 Vue.js
vue绑定class的三种方法
Dec 24 Vue.js
详解vue身份认证管理和租户管理
May 25 Vue.js
vue整合百度地图显示指定地点信息
Apr 06 Vue.js
vue3使用vuedraggable实现拖拽功能
Apr 06 Vue.js
vue判断按钮是否可以点击
Apr 09 Vue.js
vue项目打包后路由错误的解决方法
Apr 13 Vue.js
vue elementUI表格控制对应列
Apr 13 Vue.js
vue修饰符.capture和.self的区别
Apr 22 Vue.js
vue二维数组循环嵌套方式 循环数组、循环嵌套数组
Apr 24 Vue.js
vue.js 使用原生js实现轮播图
Apr 26 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
php5 apache 2.2 webservice 创建与配置(java)
2011/01/27 PHP
PHP设计模式之简单投诉页面实例
2016/02/24 PHP
PHP小偷程序的设计与实现方法详解
2016/10/15 PHP
PHP中使用jQuery+Ajax实现分页查询多功能操作(示例讲解)
2017/09/17 PHP
JavaScript 对话框和状态栏使用说明
2009/10/25 Javascript
为JavaScript提供睡眠功能(sleep) 自编译JS引擎
2010/08/16 Javascript
jquery ajax return没有返回值的解决方法
2011/10/20 Javascript
JS定时器实例
2013/04/17 Javascript
javascript 通用loading动画效果实例代码
2014/01/14 Javascript
jQuery学习笔记之jQuery+CSS3的浏览器兼容性
2015/01/19 Javascript
javascript实现客户端兼容各浏览器创建csv并下载的方法
2015/03/23 Javascript
nodejs实现获取某宝商品分类
2015/05/28 NodeJs
基于javascript实现listbox左右移动
2016/01/29 Javascript
轻松搞定jQuery.noConflict()
2016/02/15 Javascript
解决vue中对象属性改变视图不更新的问题
2018/02/23 Javascript
详解Vue中使用插槽(slot)、聚类插槽
2019/04/12 Javascript
详解vue-cli+element-ui树形表格(多级表格折腾小计)
2019/04/17 Javascript
JS实现的雪花飘落特效示例
2019/12/03 Javascript
vue element自定义表单验证请求后端接口验证
2019/12/11 Javascript
leaflet加载geojson叠加显示功能代码
2020/02/21 Javascript
Python编程中用close()方法关闭文件的教程
2015/05/24 Python
Python内置模块turtle绘图详解
2017/12/09 Python
pandas or sql计算前后两行数据间的增值方法
2018/04/20 Python
Python 词典(Dict) 加载与保存示例
2019/12/06 Python
Pycharm调试程序技巧小结
2020/08/08 Python
英国婴儿和儿童服装网站:Vertbaudet
2018/04/02 全球购物
如何唤起类中的一个方法
2013/11/29 面试题
临床医学系毕业生推荐信
2013/11/09 职场文书
党的生日活动方案
2014/08/15 职场文书
教师个人学习总结
2015/02/11 职场文书
研究生导师推荐信
2015/03/25 职场文书
MySql 8.0及对应驱动包匹配的注意点说明
2021/06/23 MySQL
开机音效回归! Windows 11重新引入开机铃声
2021/11/21 数码科技
mysql自增长id用完了该怎么办
2022/02/12 MySQL
python 使用tkinter与messagebox写界面和弹窗
2022/03/20 Python
gtx1650怎么样 gtx1650显卡相当于什么级别
2022/04/08 数码科技