核心机制
1. 数据劫持(Data Observation)
通过劫持对象属性的读写操作,在 读属性时收集依赖,在 写属性时触发更新。
2. 依赖收集(Dependency Collection)
每个被劫持的属性都有一个 Dep
实例,负责管理所有依赖(Watcher)。
3. 派发更新(Dependency Notification)
当属性被修改时,通过 Dep
通知所有依赖(Watcher)执行更新。
Vue 2 的实现(基于 Object.defineProperty)
1. 数据劫持
class Dep {
constructor() {
this.subs = new Set(); // 存储 Watcher 实例
}
depend() {
if (Dep.target) {
this.subs.add(Dep.target); // 收集依赖
}
}
notify() {
this.subs.forEach(watcher => watcher.update()); // 触发更新
}
}
Dep.target = null; // 全局变量,指向当前正在计算的 Watcher
function defineReactive(obj, key) {
const dep = new Dep();
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
dep.depend(); // 收集依赖
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
dep.notify(); // 触发更新
}
});
}
2. Watcher(依赖观察者)
class Watcher {
constructor(getter) {
this.getter = getter;
this.value = this.get();
}
get() {
Dep.target = this; // 设置全局 Watcher
const value = this.getter(); // 触发属性的 getter,收集依赖
Dep.target = null;
return value;
}
update() {
this.get(); // 更新逻辑(实际中会触发视图更新)
}
}
3. 使用示例
const data = { count: 0 };
defineReactive(data, 'count');
new Watcher(() => {
console.log('Count changed:', data.count);
});
data.count = 1; // 控制台输出: "Count changed: 1"
代码片段解析
get() {
Dep.target = this; // 关键点1:将当前 Watcher 实例挂载到全局 Dep.target
const value = this.getter();// 关键点2:执行 getter 函数,触发数据属性的 getter
Dep.target = null; // 关键点3:清除全局 Dep.target
return value;
}
核心逻辑拆解
关键点1:Dep.target = this
- 作用:将当前
Watcher
实例(例如一个组件的渲染函数、计算属性或侦听器)临时存储到全局变量Dep.target
。 - 为什么需要全局变量:
- 数据属性的
getter
被触发时,需要知道“当前是谁在访问这个属性”,这样才能将依赖(Watcher)收集到对应的Dep
中。 Dep.target
就像一个“登记处”,告诉系统:“当前正在执行的是这个 Watcher,如果访问了响应式数据,请把我和这些数据关联起来”。
- 数据属性的
关键点2:this.getter()
getter
是什么:getter
是一个函数,通常是需要观察的表达式或函数。例如:- 组件的渲染函数(访问模板中用到的数据)。
- 计算属性的计算函数(如
() => this.a + this.b
)。 - 用户自定义的
watch
回调。
- 执行
getter
的作用:- 当
getter
执行时,会访问响应式数据(如data.count
),触发数据属性的getter
。 - 数据属性的
getter
中会调用dep.depend()
,将当前全局的Dep.target
(即当前 Watcher)收集为依赖。
- 当
关键点3:Dep.target = null
- 作用:清除全局的
Dep.target
,避免后续非 Watcher 逻辑误触发依赖收集。 - 必要性:
- JavaScript 是单线程的,同一时间只能有一个 Watcher 在执行。
- 通过
Dep.target = null
,确保只有在 Watcher 的get()
方法执行期间,依赖收集才会发生。
完整流程示例
假设有以下代码:
const data = { count: 0 };
defineReactive(data, 'count'); // 将 data.count 转换为响应式
// 创建一个 Watcher,观察 data.count
const watcher = new Watcher(() => {
console.log('Current count:', data.count);
});
步骤1:初始化 Watcher
- 调用
new Watcher()
时,会执行this.get()
方法。 Dep.target
被设置为当前watcher
实例。
步骤2:执行 getter
函数
getter
函数() => { console.log(data.count); }
开始执行。- 访问
data.count
,触发其getter
。
步骤3:数据属性的 getter
逻辑
- 在
data.count
的getter
中,调用dep.depend()
。 - 此时
Dep.target
指向当前watcher
,于是将这个watcher
添加到data.count
的依赖列表(dep.subs
)中。
步骤4:完成依赖收集
getter
执行完毕,Dep.target
被重置为null
。- 此时
data.count
的依赖列表中已经记录了watcher
。
步骤5:数据变更触发更新
- 当执行
data.count = 1
时,触发data.count
的setter
。 setter
调用dep.notify()
,通知所有依赖(即watcher
)执行update()
方法。watcher.update()
会再次执行get()
方法,重新计算值并触发更新。
为什么需要这种设计?
依赖收集的动态性
- 每次执行
getter
时,访问的响应式数据可能不同(例如条件分支if (condition) a else b
)。 - 通过动态设置
Dep.target
,可以确保每次计算时,只有实际访问到的数据的依赖被收集。
避免重复依赖
- 如果
Dep.target
未被及时清除,可能导致同一个 Watcher 被重复收集到多个 Dep 中,造成不必要的更新。
类比现实场景
假设你是一个图书馆管理员(Watcher
),需要统计读者(响应式数据)的借阅记录:
-
登记身份(
Dep.target = this
):- 你进入图书馆时,在登记处写下自己的名字(相当于设置
Dep.target
),表示“当前是我在借书”。
- 你进入图书馆时,在登记处写下自己的名字(相当于设置
-
借书操作(执行
getter
):- 你借了一本书《Vue 原理》(访问
data.count
),图书馆系统会自动记录:“这本书被管理员借走了”。
- 你借了一本书《Vue 原理》(访问
-
离开图书馆(
Dep.target = null
):- 你离开时擦掉登记处的名字,确保后续其他人的借阅不会被误记录到你名下。