Vue双向绑定原理
MVVM 数据双向绑定
MVVM
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
通过实现以下 4 个步骤,来实现数据的双向绑定:
1、实现一个监听器 Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
2、实现一个订阅器 Dep
,用来收集订阅者,对监听器 Observer
和 订阅者 Watcher
进行统一管理;
3、实现一个订阅者 Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;
4、实现一个解析器 Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
以上四个步骤的流程图表示如下:
监听器 Observer 实现
监听器Observer的底层原理是Object.defineProperty,传入一个对象,实现对此对象的监听,包括以下:
- 遍历对象的所有属性,都增加get/set方法
- 通过递归调用observer实现对对象的深度监听
- 如果给属性赋的新值是一个对象,对这个新值递归observer 实现监听
具体看Object.defineProperty与Proxy
class Observer {
constructor(data) {
this.observer(data);
}
observer(obj) {
if (obj && typeof obj === 'object') {
// 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
for (let key in obj) {
this.defineRecative(obj, key, obj[key])
}
}
}
// obj: 需要操作的对象
// attr: 需要新增get/set方法的属性
// value: 需要新增get/set方法属性的取值
defineRecative(obj, attr, value) {
// 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
this.observer(value);
Object.defineProperty(obj, attr, {
get() {
return value;
},
set: (newValue) => {
if (value !== newValue) {
// 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
this.observer(newValue);
value = newValue;
console.log('监听到数据的变化');
}
}
})
}
}
实现解析器Compile
通过监听器 Observer
订阅器 Dep
和订阅者 Watcher
的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom
节点,而是直接固定某个节点进行替换数据的。
compile主要做的事情是:
- 在生命周期的initState方法中将data,prop,watch、computed等通过observe方法(底层是Object.defineProperty)数据劫持,
- 在inintRender中解析模板指令,将模板中的指令v-model、v-text、{{}}}和生成Watcher实例,通过watcher实例将指令与数据建立依赖(用Dep.target实现添加Watcher进入Dep容器)
- 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图.
使用观察者设计模式,创建Dep和Wather类
1,在解析模板,收集data中各个属性在模板中被引用的dom节点集合,当该数据改变时,更新依赖了该数据的dom节点集合,就实现了数据驱动页面更新。
2,创建Dep类和Watcher类
- Dep:
用于收集某个data属性依赖的dom节点集合,并提供更新方法
- Watcher:
每个dom节点的包裹对象
订阅器 Dep 实现
发布 —订阅设计模式
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。
在数据被读或写的时候通知那些依赖该数据的视图更新,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是前一节所说的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
现在,我们需要创建一个依赖收集容器,也就是消息订阅器 Dep
,用来容纳所有的“订阅者”。订阅器 Dep
主要负责收集订阅者,然后当数据变化的时候后遍历执行订阅者的更新函数。
Watcher和Observer之间的纽带,每一个Oserver都有一个Dep订阅器实例,用来存储订阅者Watcher
Dep总结(自己总结的,可能有误):
- 通过var dep = new Dep(),新建个数组容器subs,用于收集某个data属性依赖的dom节点(收集订阅者Watcher)集合
- 如何收集(依赖)订阅者Watcher:
- 在Vue实例初始化时,Compile解析模板,将V-modol、{{}}等指令解析并创建订阅者Watcher对象
- Watcher对象在初始化时的构造函数会调用Watcherget()方法,Watcherget()方法中将Dep.target指向该dom节点,然后强行触发Object.definProperty的getter方法
- 在getter方法里面添加Dep.target到数组subs(即添加订阅者)就完成了一次收集。因为每次触发getter之前都对该静态变量赋值,所以不存在收集错依赖的情况。
- 当数据变化时,执行notify()方法来遍历执行容器内所有Watcher的更新函数
部分简化源码理解:
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];//更新的话不会再绑定target,就不会再加入订阅器
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
if(Dep.target) dep.addSub(Dep.target);
return val;
}
// ... 省略
});
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 调用订阅者的update方法,通知变化
});
}
};
订阅者 Watcher 实现
订阅者 Watcher
分析如下:
订阅者 Watcher
是一个 类,在它的构造函数中,定义了一些属性:
- **vm:**一个 Vue 的实例对象;
- **exp:**是
node
节点的v-model
等指令的属性值 或者插值符号中的属性。如v-model="name"
,exp
就是name
; - **cb:**是
Watcher
绑定的更新函数;
W
atcher
在初始化的时候将自己添加进订阅器 Dep
的步骤:
注意:只需要在订阅者 Watcher
初始化的时候才需要添加订阅者,所以需要做一个判断操作,用Dep.target来进行判断。
- 实例化一个渲染
watcher
的时候,首先进入watcher
的构造函数逻辑,就会执行它的this.get()
方法,进入get
函数,首先会执行:- Dep.target = this; // 将自己赋值为全局的订阅者
- let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
- 在这个过程中会对
vm
上的数据访问,其实就是为了触发监听器中的getter
。
在getter方法里面添加Dep.target到数组subs(即添加订阅者)就完成了一次收集。完成依赖收集后,还需要把Dep.target
恢复null状态。 - 因为每次触发getter之前都对该静态变量赋值,所以不存在收集错依赖的情况。
订阅器的update()函数
- 在监听器setter()监听到属性变化时,调用dep.notify()函数,notify将遍历subs容器里的所有订阅器Watcher的update函数执行。
update()
函数是用来当数据发生变化时调用Watcher
自身的更新函数进行更新的操作- 先通过
let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前get()
获得的旧数据进行比较,如果不一样,则调用回调函数cb
进行更新。
具体步骤
第一步:
需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么久能监听到了数据变化
第二步:
compile解析横板令,将模板中的变量替换成数据.然后初始化渲染页面视图,并将每个令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:
Watcher订阅名是 observer和 Compile之间通信的桥梁,主要做的事情是:
1.在自身实例化时往属性订倒器(dep)里面添加自己
2.自身必须有一个 update()方法
3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中定的回调,则功成身退
第四步:
MVVM作为数据绑定的入口,合 observer、 Compile和 Watcher三者,通过 Observer来监听自己的model数据変化,通过 Compile来解析编译模板指令,最终利用 Watcher搭起 Observer和 Compile之间的通信标梁,达到数据变化->视图更新新:视图交互变化(Input)->数据mode变更的双向绑定效果。
Observer
将一个正常的object转换成每个层次都是响应式
数组的响应式处理
正因为我们可以通过Array原型上的方法来改变数组的内容,所以ojbect那种通过getter/setter的实现方式就行不通了。
ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法。
Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理
这些方法分别是:push
、pop
、shift
、unshift
、splice
、sort
、reverse
这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码
思路:就是 以Array.prototype为原型,创建一个新对象arrayMthods
然后在新对象arrayMthods上定义(改写)这些方法
定义 数组 的原型指向 arrayMthods
这就相当于用一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。在拦截器中使用原生Array的原型方法去操作数组。
import def from "./def";
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
// 备份原来的方法
const original = arrayPrototype[methodName];
// 定义新的方法
def(
arrayMethods,
methodName,
function () {
console.log("array数据已经被劫持");
// 恢复原来的功能(数组方法)
const result = original.apply(this, arguments);
// 把类数组对象变成数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来
// 在拦截器中获取Observer的实例
const ob = this.__ob__;
// 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
let inserted = [];
switch (methodName) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
// 查看有没有新插入的项inserted,有的话就劫持
if (inserted) {
ob.observeArray(inserted);
}
return result;
},
false
);
});
参考:
https://juejin.cn/post/6844903903822086151
https://segmentfault.com/a/1190000006599500
https://juejin.cn/post/7065967379095748638#heading-6