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

一句话概括:数据劫持(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查看一下

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双向数据绑定内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue2实现组件props双向绑定

    Vue学习笔记-3 前言 Vue 2.x相比较Vue 1.x而言,升级变化除了实现了Virtual-Dom以外,给使用者最大不适就是移除的组件的props的双向绑定功能. 以往在Vue1.x中利用props的twoWay和.sync绑定修饰符就可以实现props的双向绑定功能,但是在Vue2中彻底废弃了此功能,如果需要双向绑定需要自己来实现. Vue2的组件props通信方式 在Vue2中组件的props的数据流动改为了只能单向流动,即只能由组件外(调用组件方)通过组件的DOM属性attribu

  • vue2.0数据双向绑定与表单bootstrap+vue组件

    最近一直在用vue,觉得确实是好用. 一,拿数据的双向绑定来说吧 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo1</title> </head> <body> <div id="app"> {{ name }} <input typ

  • 解析Vue2.0双向绑定实现原理

    一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向数据绑定的做法大致有如下三种: 1.发布者-订阅者模式(backbone.js) 思路:使用自定义的data属性在HTML代码中指明绑定.所有绑定起来的JavaScript对象以及DOM元素都将"订阅"一个发布者对象.任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素. 2.脏值检查(angular

  • 详解如何在Vue2中实现组件props双向绑定

    Vue学习笔记-3 前言 Vue 2.x相比较Vue 1.x而言,升级变化除了实现了Virtual-Dom以外,给使用者最大不适就是移除的组件的props的双向绑定功能. 以往在Vue1.x中利用props的twoWay和.sync绑定修饰符就可以实现props的双向绑定功能,但是在Vue2中彻底废弃了此功能,如果需要双向绑定需要自己来实现. Vue2的组件props通信方式 在Vue2中组件的props的数据流动改为了只能单向流动,即只能由组件外(调用组件方)通过组件的DOM属性attribu

  • Vue2.x和Vue3.x的双向绑定原理详解

    双向的绑定的原理 通过Object.defineproperty()重新定义对象属性的set方法.get方法来实现的,从这个属性中取值时会触发get方法,改变这个属性时会触发set方法,所以我们只要将一些需要更新view的方法放在这里面就可以实现data更新view了,而view更新data其实可以通过事件监听实现 当视图上的数据发生改变时, data 中的数据也发生改变当 data 中的数据发生改变时,视图中的数据也发生改变 Object.defineProperty() Object.def

  • Vue2.0利用 v-model 实现组件props双向绑定的优美解决方案

    在项目中开始使用vue2来构建项目了,跟 vue1 很大的一处不同在于2 取消了props 的双向绑定,改成只能从父级传到子级的单向数据流,初衷当然是好的,为了避免双向绑定在项目中容易造成的数据混乱. 解决方案一 然后开始参考网上和github上的方案等等,发现很多解决方案是这样的 用data对象中创建一个props属性的副本 watch props属性 赋予data副本 来同步组件外对props的修改 watch data副本,emit一个函数 通知到组件外 这里以最常见的 modal为例子:

  • Vue2.0实现组件数据的双向绑定问题

    通过上一节的学习,我们了解到了在Vue的组件中数据传递: prop 向下传递,事件向上传递 .意思是父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息.但Vue中, props 是单向数据绑定,虽然在Vue 1.0版本中,通过 .sync 能实现双向数据绑定.但 .sync 在几个版本中被移除,尽管在2.3版本重新引入 .sync 修饰符,可这次引入只是作为一个编译时的语法糖存在.如果直接使用 .sync 修饰符来做双向数据绑定,会报警告信息.那么我们如何在组件中实现双向数据

  • 解决Vue2.x父组件与子组件之间的双向绑定问题

    最近在研究如何写一套基于Vue2.x的UI组件给自己用,提升一点BIG,在制作含有input的组件遇到一个问题:不知怎样才能把子组件中input与调用者(父组件)的数据实现双向绑定,想过使用Vuex,但观摩了一下其他优秀的UI框架,发现使用Vuex会给其他使用者造成麻烦,于是决心找到寻求解决方法,在参考了几篇大牛们文章后,终于找到. 在这将解决方案贴出,希望能帮助到和我一样初次接触Vue这个框架的同行们. 子组件的代码逻辑 <template> <div class="ne-i

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

    一句话概括:数据劫持(Object.defineProperty)+发布订阅模式 双向数据绑定有三大核心模块(dep .observer.watcher),它们之间是怎么连接的,下面来一一介绍. 为了大家更好的理解双向数据绑定原理以及它们之间是如何实现关联的,先带领大家复习一下发布订阅模式. 一.首先了解什么是发布订阅模式 直接上代码: 一个简单的发布订阅模式,帮助大家更好的理解双向数据绑定原理 //发布订阅模式 function Dep() { this.subs = []//收集依赖(也就是

  • Vue2.0 $set()的正确使用详解

    vue2.0 给data对象新增属性,并触发视图更新 如下代码,给 student对象新增 age 属性 data () { return { student: { name: '', sex: '' } } } 众所周知,直接给student赋值操作,虽然可以新增属性,但是不会触发视图更新 mounted () { this.student.age = 24 } 原因是:受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除.因为 Vue.js 在初始化实例时将属性转为 getter

  • Vue2 的 diff 算法规则原理详解

    目录 前言 算法规则 diff 优化策略 老数组的开始与新数组的开始 老数组的结尾与新数组的结尾 老数组的开始与新数组的结尾 老数组的结尾与新数组的开始 以上四种情况都没对比成功 推荐在渲染列表时为节点设置 key 循环比对结束的后续处理工作 源码解析 总结 前言 所谓 diff 算法,就是通过比对新旧两个虚拟节点不一样的地方,针对那些不一样的地方进行新增或更新或删除操作.接下来我们详细介绍节点更新的过程. 首先进行静态节点处理,判断新旧两个虚拟节点是否是静态节点,如果是,就不需要进行更新操作,

  • 基于vue2.0动态组件及render详解

    如下所示: <template> <div class="hello"> <h1>{{ msg }}</h1> <h2>这里是Boor</h2> <component v-bind:my-data="items" v-bind:is="currentView"> <!-- 组件在 vm.currentview 变化时改变! --> </compo

  • vue2.0自定义指令示例代码详解

    1.什么是指令? 指令通常以"v-"作为前缀, 以方便Vue知道你在使用一种特殊的标记. 除了 Vue 核心携带着的一些默认指令(v-model 和 v-show)之外, Vue 还允许你注册自己的自定义指令.某些情况下,还是需要对普通元素进行一些底层 DOM 访问, 这也是自定义指令仍然有其使用场景之处. 2.全局指令: 当页面加载时,元素将获取焦点,事实上,在访问页面时,如果你还没有点击任何地方,上面的输入框现在应该处于获取焦点的状态.现在让我们构建指令以完成此效果: <te

  • AngularJS入门教程之数据绑定原理详解

    本文实例讲述了AngularJS数据绑定原理.分享给大家供大家参考,具体如下: 注 这篇文章主要是写给新手的,是给那些刚刚开始接触Angular,并且想了解数据帮定是如何工作的人.如果你已经对Angular比较了解了,那强烈建议你直接去阅读源代码. Angular用户都想知道数据绑定是怎么实现的.你可能会看到各种各样的词汇:$watch,$apply,$digest,dirty-checking...它们是什么?它们是如何工作的呢?这里我想回答这些问题,其实它们在官方的文档里都已经回答了,但是我

  • AngularJS框架中的双向数据绑定机制详解【减少需要重复的开发代码量】

    本文实例讲述了AngularJS框架双向数据绑定机制.分享给大家供大家参考,具体如下: 之前写的一篇<AngularJS入门示例之Hello World详解> ,介绍ng-model的时候提到:使用AngularJS的双向数据绑定机制,不需要我们编写繁琐的代码来实现同样的功能.现在我们看一个比较震撼的例子,看看angularJS是如何减少我们在前端开发中的繁琐劳动的.越是感受到框架功能的强大,越是能够激发学习的兴趣和动力. 假如我们有一个学生信息列表,包含学生的姓名.地址和年龄信息.假如这个数

  • Vue3.0数据响应式原理详解

    基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理 预备知识 ES6 Proxy,整个响应式系统的基础. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档. 先把Vue3.0跑起来 先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可. 下面一个简单计数器的DEMO: <!DOCTY

  • vue2.0全局组件之pdf详解

    目的:像elementUI那样注册全局组件 预览pdf文件 技术支持:使用火狐的pdf.js http://mozilla.github.io/pdf.js/ 准备:新建一个CPdf.vue文件,把火狐demo里面的build里面的pdf.js下载来,并且依赖了elementUI开发的其实就是用了<el-button> 编写: template <template> <div class="cpdf"> <div class="cen

随机推荐