依赖追踪基本原理
- 数据劫持:Vue 使用
Object.defineProperty()
方法对数据对象的属性进行劫持,在属性的 getter
和 setter
中植入依赖收集和触发更新的逻辑。当访问属性时,会触发 getter
,从而进行依赖收集;当修改属性时,会触发 setter
,进而通知依赖进行更新。
- 依赖收集:每个被劫持的属性都对应一个
Dep
实例(依赖管理器),在 getter
中,会将当前正在运行的 Watcher
(订阅者)添加到 Dep
中,从而建立起数据与依赖它的 Watcher
之间的关系。
依赖追踪实现方式
- 使用
Object.defineProperty()
:在 Vue 2.x 中,通过遍历对象的属性,使用 Object.defineProperty()
为每个属性定义 getter
和 setter
。例如:
function defineReactive(data, key, value) {
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
dep.notify();
}
});
}
- ES6 Proxy(Vue 3.x):Vue 3.x 采用了
Proxy
来替代 Object.defineProperty()
。Proxy
可以直接代理整个对象,而不需要对每个属性进行遍历定义,在性能和功能上更强大。例如:
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
if (Dep.target) {
const dep = target.__dep__ || (target.__dep__ = new Dep());
dep.addSub(Dep.target);
}
return target[key];
},
set(target, key, value) {
target[key] = value;
const dep = target.__dep__;
if (dep) {
dep.notify();
}
return true;
}
});
};
Watcher 和 Dep 之间的关联与工作
- 关联:
Watcher
实例化时,会将自身赋值给 Dep.target
,然后访问数据属性,触发属性的 getter
,在 getter
中把 Dep.target
(即当前 Watcher
)添加到 Dep
的 subs
数组(存储所有订阅者)中,这样就建立了 Watcher
与 Dep
的关联。
- 工作流程:
- 依赖收集阶段:当组件渲染时,会触发数据属性的
getter
,此时 Dep.target
是正在渲染组件的 Watcher
,属性的 Dep
实例将该 Watcher
添加到自己的订阅者列表 subs
中。
- 更新阶段:当数据属性发生变化,触发
setter
,setter
会调用 Dep
实例的 notify
方法,notify
方法遍历 subs
数组,调用每个 Watcher
的 update
方法,Watcher
的 update
方法会重新计算组件的状态,从而触发组件的重新渲染。例如:
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}