每当被问到Vue数据双向绑定原理的时候,你们可能都会脱口而出:Vue内部经过Object.defineProperty
方法属性拦截的方式,把data
对象里每一个数据的读写转化成getter
/setter
,当数据变化时通知视图更新。虽然一句话把大概原理归纳了,可是其内部的实现方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue内部双向绑定原理的实现过程。javascript
所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。以下图:
html
也就是说:vue
要实现这两个过程,关键点在于数据变化如何更新视图,由于视图变化更新数据咱们能够经过事件监听的方式来实现。因此咱们着重讨论数据变化如何更新视图。java
数据变化更新视图的关键点则在于咱们如何知道数据发生了变化,只要知道数据在何时变了,那么问题就变得迎刃而解,咱们只需在数据变化的时候去通知视图更新便可。node
数据的每次读和写可以被咱们看的见,即咱们可以知道数据何时被读取了或数据何时被改写了,咱们将其称为数据变的‘可观测’。git
要将数据变的‘可观测’,咱们就要借助前言中提到的Object.defineProperty
方法了,关于该方法,MDN上是这么介绍的:github
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组
在本文中,咱们就使用这个方法使数据变得“可观测”。缓存
首先,咱们定义一个数据对象car
:函数
let car = { 'brand':'BMW', 'price':3000 }
咱们定义了这个car
的品牌brand
是BMW
,价格price
是3000。如今咱们能够经过car.brand
和car.price
直接读写这个car
对应的属性值。可是,当这个car
的属性被读取或修改时,咱们并不知情。那么应该如何作才可以让car
主动告诉咱们,它的属性被修改了呢?
接下来,咱们使用Object.defineProperty()
改写上面的例子:
let car = {} let val = 3000 Object.defineProperty(car, 'price', { get(){ console.log('price属性被读取了') return val }, set(newVal){ console.log('price属性被修改了') val = newVal } })
经过Object.defineProperty()
方法给car
定义了一个price
属性,并把这个属性的读和写分别使用get()
和set()
进行拦截,每当该属性进行读或写操做的时候就会出发get()
和set()
。以下图:
能够看到,car
已经能够主动告诉咱们它的属性的读写状况了,这也意味着,这个car
的数据对象已是“可观测”的了。
为了把car
的全部属性都变得可观测,咱们能够编写以下两个函数:
/** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */ function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive (obj,key,val) { Object.defineProperty(obj, key, { get(){ console.log(`${key}属性被读取了`); return val; }, set(newVal){ console.log(`${key}属性被修改了`); val = newVal; } }) }
如今,咱们就能够这样定义car
:
let car = observable({ 'brand':'BMW', 'price':3000 })
car
的两个属性都变得可观测了。
完成了数据的'可观测',即咱们知道了数据在何时被读或写了,那么,咱们就能够在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,咱们须要先将全部依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
如今,咱们须要建立一个依赖收集容器,也就是消息订阅器Dep,用来容纳全部的“订阅者”。订阅器Dep主要负责收集订阅者,而后当数据变化的时候后执行对应订阅者的更新函数。
建立消息订阅器Dep:
class Dep { constructor(){ this.subs = [] }, //增长订阅者 addSub(sub){ this.subs.push(sub); }, //判断是否增长订阅者 depend () { if (Dep.target) { this.addSub(Dep.target) } }, //通知订阅者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
有了订阅器,再将defineReactive
函数进行改造一下,向其植入订阅器:
function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key}属性被读取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}属性被修改了`); dep.notify() //数据变化通知全部订阅者 } }) }
从代码上看,咱们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里须要特别注意的是它有一个静态属性 target
,这是一个全局惟一 的Watcher
,这是一个很是巧妙的设计,由于在同一时间只能有一个全局的 Watcher
被计算,另外它的自身属性 subs
也是 Watcher
的数组。
咱们将订阅器Dep添加订阅者的操做设计在getter
里面,这是为了让Watcher
初始化时进行触发,所以须要判断是否要添加订阅者。在setter
函数里面,若是数据变化,就会去通知全部订阅者,订阅者们就会去执行对应的更新的函数。
到此,订阅器Dep设计完毕,接下来,咱们设计订阅者Watcher.
订阅者Watcher
在初始化的时候须要将本身添加进订阅器Dep
中,那该如何添加呢?咱们已经知道监听器Observer
是在get
函数执行了添加订阅者Wather
的操做的,因此咱们只要在订阅者Watcher
初始化的时候出发对应的get
函数去执行添加订阅者操做便可,那要如何触发get
的函数,再简单不过了,只要获取对应的属性值就能够触发了,核心缘由就是由于咱们使用了Object.defineProperty( )
进行数据监听。这里还有一个细节点须要处理,咱们只要在订阅者Watcher
初始化的时候才须要添加订阅者,因此须要作一个判断操做,所以能够在订阅器上作一下手脚:在Dep.target
上缓存下订阅者,添加成功后再将其去掉就能够了。订阅者Watcher
的实现以下:
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 将本身添加到订阅器的操做 }, update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); }, get(){ Dep.target = this; // 缓存本身 let value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放本身 return value; } }
过程分析:
订阅者Watcher
是一个 类,在它的构造函数中,定义了一些属性:
node
节点的v-model
或v-on:click
等指令的属性值。如v-model="name"
,exp
就是name
;Watcher
绑定的更新函数;当咱们去实例化一个渲染 watcher
的时候,首先进入 watcher
的构造函数逻辑,就会执行它的 this.get()
方法,进入 get
函数,首先会执行:
Dep.target = this; // 缓存本身
实际上就是把 Dep.target
赋值为当前的渲染 watcher
,接着又执行了:
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
在这个过程当中会对 vm
上的数据访问,其实就是为了触发数据对象的getter
。
每一个对象值的 getter
都持有一个 dep
,在触发 getter
的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher
订阅到这个数据持有的 dep
的 subs
中,这个目的是为后续数据变化时候能通知到哪些 subs
作准备。
这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并无,完成依赖收集后,还须要把 Dep.target
恢复成上一个状态,即:
Dep.target = null; // 释放本身
由于当前vm
的数据依赖收集已经完成,那么对应的渲染Dep.target
也须要改变。
而update()
函数是用来当数据发生变化时调用Watcher
自身的更新函数进行更新的操做。先经过let value = this.vm.data[this.exp];
获取到最新的数据,而后将其与以前get()
得到的旧数据进行比较,若是不同,则调用更新函数cb
进行更新。
至此,简单的订阅者Watcher
设计完毕。
完成以上工做后,咱们就能够来真正的测试了。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1 id="name"></h1> <input type="text"> <input type="button" value="改变data内容" onclick="changeInput()"> <script src="observer.js"></script> <script src="watcher.js"></script> <script> function myVue (data, el, exp) { this.data = data; observable(data); //将数据变的可观测 el.innerHTML = this.data[exp]; // 初始化模板数据的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; } var ele = document.querySelector('#name'); var input = document.querySelector('input'); var myVue = new myVue({ name: 'hello world' }, ele, 'name'); //改变输入框内容 input.oninput = function (e) { myVue.data.name = e.target.value } //改变data内容 function changeInput(){ myVue.data.name = "难凉热血" } </script> </body> </html>
observer.js
/** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */ function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key}属性被读取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}属性被修改了`); dep.notify() //数据变化通知全部订阅者 } }) } class Dep { constructor(){ this.subs = [] } //增长订阅者 addSub(sub){ this.subs.push(sub); } //判断是否增长订阅者 depend () { if (Dep.target) { this.addSub(Dep.target) } } //通知订阅者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
watcher.js
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 将本身添加到订阅器的操做 } get(){ Dep.target = this; // 缓存本身 let value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放本身 return value; } update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } } }
效果:
完整代码,请戳这里☞ vue数据双向绑定原理及实现
总结一下:
实现数据的双向绑定,首先要对数据进行劫持监听,因此咱们须要设置一个监听器Observer
,用来监听全部属性。若是属性发上变化了,就须要告诉订阅者Watcher
看是否须要更新。由于订阅者是有不少个,因此咱们须要有一个消息订阅器Dep
来专门收集这些订阅者,而后在监听器Observer
和订阅者Watcher
之间进行统一管理的。
(完)