为什么要依赖收集?
我们可以通过一些简单的例子来理解依赖收集的必要性。
栗子 🌰 1:视图不需要用到的数据更新
假设我们有一个 Vue 实例:
new Vue({
template:
`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
<div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
如果我们更新 text3
:
this.text3 = 'modify text3';
text3
没有出现在视图中,所以完全没有必要触发视图更新。依赖收集能避免这样的浪费——只更新视图中用到的变量!
栗子 🌰 2:多个实例依赖相同的全局对象
假设我们有一个全局对象,多个 Vue 实例都在视图中引用了它的数据:
let globalObj = {
text1: 'text1'
};
let o1 = new Vue({
template: `<div><span>{{text1}}</span></div>`,
data: globalObj
});
let o2 = new Vue({
template: `<div><span>{{text1}}</span></div>`,
data: globalObj
});
当我们修改 globalObj.text1
的值:
globalObj.text1 = 'hello, text1';
因为 text1
被多个 Vue 实例依赖,Vue 的响应式系统需要确保这两个实例都更新它们的视图。这就需要“依赖收集”来记录哪些实例依赖于哪些数据,当数据变更时,通知它们更新。
实现依赖收集:订阅者 Dep
Dep
是一个用于存放 Watcher
的类,它的作用是记录依赖,并在数据发生变化时通知这些依赖去更新视图。
class Dep {
constructor () {
/* 用来存放 Watcher 对象的数组 */
this.subs = [];
}
/* 在 subs 中添加一个 Watcher 对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有 Watcher 对象更新视图 */
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
addSub
:将Watcher
添加到subs
数组中,订阅当前数据的变化。notify
:在数据发生变化时调用,通知所有订阅者更新。
观察者 Watcher
Watcher
类用来表示一个依赖,它会记录数据被访问的位置,当数据更新时通知视图更新。
class Watcher {
constructor () {
/* 在实例化 Watcher 时,将其赋值给 Dep.target,用于依赖收集 */
Dep.target = this;
}
/* 更新视图的方法 */
update() {
console.log("视图更新啦~");
}
}
Dep.target = null;
Watcher
实例化时会将自己赋值给 Dep.target
,让 Dep
记录当前依赖。
依赖收集实现
接下来我们修改 defineReactive
函数,将 Dep
与 Watcher
结合,实现依赖收集。
在闭包中增加 Dep
对象,用来收集 Watcher
。当 get
被触发时,当前 Watcher
会被加入 Dep
的订阅队列。之后,set
会调用 Dep.notify()
,通知所有的 Watcher
去更新视图。
function defineReactive(obj, key, val) {
/* 创建一个 Dep 实例 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
/* 将 Dep.target(当前 Watcher)添加到 dep 的 subs 中 */
dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
/* 触发 dep 的 notify 方法,通知所有 Watcher 更新视图 */
dep.notify();
}
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 创建一个 Watcher 实例,Dep.target 将指向这个 Watcher */
new Watcher();
/* 模拟 render 过程,触发 get 以便依赖收集 */
console.log('render~', this._data.text1);
}
}
在这个例子中,defineReactive
方法通过 Object.defineProperty
实现数据的 get
和 set
拦截,并在 get
时记录依赖 Watcher
。当数据变化时,触发 notify
,通知所有 Watcher
更新视图。
小结
Vue 的响应式系统依赖于 依赖收集 与 订阅更新。
- 依赖收集:在
get
中记录当前依赖(Watcher
),并将它存入Dep
的subs
数组。 - 触发更新:当数据变化时,在
set
中调用Dep
的notify
,通知所有依赖更新视图。
通过这种机制,Vue 实现了高效、自动化的视图更新,使得响应式系统能够精准追踪数据变化与依赖关系,轻松完成视图更新。理解这些原理之后,Vue 响应式系统背后的“魔法”其实一点都不复杂!