响应式,顾名思义,一个地方改变,触发另一个地方改变,在vue的体现就是,data的某个属性值改变,自动触发了视图更新
说到响应式,可以谈谈观察者模式
观察者模式是一种实现一对多关系解耦的行为设计模式。它主要涉及两个角色:观察目标、观察者
class Watcher {
constructor(name) {
this.name = name;
}
receiveMessage(message) {
console.log(`${this.name}收到消息:${message}`);
}
}
class Phone {
constructor() {
this.watchers = [];
}
addWatcher(watcher) {
this.watchers.push(watcher)
return this;
}
notify(message) {
this.watchers.forEach(watcher => {
watcher.receiveMessage(message);
})
}
}
let phone = new Phone();
let watcher1 = new Watcher('监听者1');
let watcher2 = new Watcher('监听者2');
phone.addWatcher(watcher1).addWatcher(watcher2);
phone.notify('开饭了');
上面的例子中,Watcher是观察者,Phone是被观察目标。可以看出,想要实现一个观察者模式有2个要点
- 维护一个观察者队列
- 通知消息
vue响应式原理跟上面的思想是一样的,最大的区别是,上面我们显式地调用了addWatcher来添加监听者、调用了notify来触发更新,而开发中只需要改变data某个属性值就可以触发视图更新,很明显,这些事情都是vue内部帮我们做了。
通常我们把添加监听者,叫做依赖收集。
Object.defineProperty 相信大家都不陌生了,显然,vue要做的就是遍历data里的数据 ,通过defineProperty,在 get 里收集依赖,在 set 派发更新。
这是删减版的defineReactive函数
function defineReactive$$1(
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep(); //{subs:[renderWatcher,computedWatcher1,computedWatcher2]}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();// 依赖收集
}
return value
},
set: function reactiveSetter(newVal) {
val = newVal;
dep.notify(); //通知更新
}
});
}
- 函数里有一个dep对象,用于辅助管理观察者队列,它有一个subs属性,subs里的项是watcher
- dep.depend就是把当前的watcher对象收集到subs里,对应观察者模式的addWatcher
- dep.notify就是执行subs里的每一个watcher对象里的更新方法,对应观察者模式的派发更新
关于Watcher类,有三类,为renderWatcher 、computedWatcher、userCustomWatcher。
- renderWatcher就是更新时会触发render的watcher
- computedWatcher就是遍历computed对象,生成的watcher
- userCustomWatche就是r遍历watch对象,生成的watcher
何时触发 get 函数?
对于computedWatcher、userCustomWatcher,很明显,执行我们定义的函数,就会访问到我们书写的变量,从而触发 get ;
而renderWatcher可能容易弄混,因为我们一般不直接书写render函数,而是写template,我一开始就以为编译template时就会触发get。。。但其实这是两个完全独立的过程,编译可在运行时,也可预编译,最终都会把template转成render函数(准确说时render函数字符串)。
这里有个调试小技巧,在get里打一个debugger
这样你就能在右侧面板的Call Stack清楚的看到谁触发了它,过程中发生了什么,而不会看着看着就不知道被绕进哪了,迷失在茫茫代码中。
现在我们大概可以总结vue响应式原理
- 遍历data,拦截对象属性的get和set方法 (数组是劫持数组原型方法)
- 对于每一个属性,闭包里有一个dep对象,它有subs数组,每一项都是一个watcher
- 访问data的某个属性会触发get,这里进行依赖收集,就是往subs里添加watcher
- 修改data的某个属性会触发set,这里进行派发更新,就是执行subs里所有watcher的更新方法