在 Vue 应用中,你是否好奇:
"当我修改
this.message
时,DOM 为何能自动更新?" "为什么只有被模板用到的数据才会触发更新?" "Vue 是如何知道哪个组件依赖哪个数据的?"
这一切的背后,是 Vue 依赖收集(Dependency Collection) 的精妙设计。
本文将从 Object.defineProperty 到 Dep-Watcher 模型,彻底解析 Vue 2 的响应式原理。
一、核心结论:依赖收集 = 数据 ↔ 视图 的双向绑定
text
数据变化 → 通知视图更新
↑ ↓
收集 触发 getter
- 谁收集?
Dep
(依赖中心) - 被谁收集?
Watcher
(观察者) - 何时收集? 组件渲染时读取数据(触发 getter)
二、三大核心角色
🎯 1. defineReactive
:让数据"响应式"
js
function defineReactive(obj, key, val) {
// 每个属性都有一个独立的依赖中心
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// ✅ 依赖收集:谁在读我?
if (Dep.target) {
dep.depend(); // 通知 dep:当前 watcher 依赖我
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// ✅ 派发更新:通知所有依赖者
dep.notify();
}
});
}
💥
dep
是每个属性的"私人秘书",记录谁依赖它。
🎯 2. Dep
:依赖管理中心
js
class Dep {
static target = null; // 🌟 全局唯一,指向当前正在计算的 Watcher
subs = []; // 存储所有依赖此数据的 Watcher
// 被收集:当前 Watcher 依赖我
depend() {
if (Dep.target) {
Dep.target.addDep(this); // 告诉 Watcher:你依赖我
}
}
// 添加订阅者
addSub(watcher) {
this.subs.push(watcher);
}
// 派发更新:数据变了!
notify() {
// 避免在 notify 时修改数组
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update(); // 通知每个 Watcher 更新
}
}
}
🔑
Dep.target
是关键:它确保同一时间只有一个 Watcher 在收集依赖。
🎯 3. Watcher
:观察者(组件/计算属性)
js
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.getter = expOrFn; // 如 vm._update(vm._render())
this.cb = cb;
this.deps = []; // 记录依赖了哪些 dep
this.depIds = new Set(); // 去重
this.value = this.get(); // 🚀 首次执行,触发依赖收集
}
// 读取数据,触发 getter
get() {
pushTarget(this); // 设置当前 Watcher
const value = this.getter.call(this.vm, this.vm);
popTarget(); // 清除
return value;
}
// 被 dep 收集
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this); // dep 记录我
}
}
// 更新:数据变化后调用
update() {
queueWatcher(this); // 异步更新
}
run() {
const value = this.get(); // 重新计算
this.cb(value, this.value); // 执行回调(如更新 DOM)
this.value = value;
}
}
💡
Watcher
是"消费者",它知道自己依赖哪些数据。
三、依赖收集全过程(图文解析)
🔄 阶段 1:初始化响应式数据
js
// data: { message: 'Hello' }
defineReactive(data, 'message', 'Hello');
// → 为 message 创建 dep 实例
text
data.message
↓
dep (subs: [])
🔄 阶段 2:组件挂载,创建 Watcher
js
new Watcher(vm, () => {
vm._update(vm._render());
});
Watcher.get()
被调用;pushTarget(this)
→Dep.target = watcher
。
text
Dep.target → Watcher实例
🔄 阶段 3:渲染触发 getter,完成收集
js
vm._render(); // 生成 VNode
// 模板中:{{ message }}
// → 读取 this.message → 触发 getter
js
// getter 执行
get() {
if (Dep.target) {
dep.depend(); // dep.depend()
}
return val;
}
js
// dep.depend()
depend() {
Dep.target.addDep(this); // watcher.addDep(dep)
}
js
// watcher.addDep(dep)
addDep(dep) {
this.deps.push(dep);
dep.addSub(this); // dep.subs.push(watcher)
}
✅ 收集完成!
text
data.message
↓
dep (subs: [watcher])
↑
Watcher (deps: [dep])
🔄 阶段 4:数据变化,派发更新
js
this.message = 'World'; // 触发 setter
js
// setter 执行
set(newVal) {
val = newVal;
dep.notify(); // 通知所有 subs
}
js
// dep.notify()
notify() {
this.subs.forEach(watcher => watcher.update());
}
→ queueWatcher(watcher)
→ 异步更新 DOM。
四、实战演示:一个简单的响应式系统
js
// 1. 数据
const data = { count: 0 };
// 2. 响应式化
defineReactive(data, 'count', 0);
// 3. 创建 Watcher(模拟组件)
new Watcher(null, () => {
console.log('Render:', data.count);
}, null);
// → 触发 getter → 依赖收集完成
// 4. 修改数据
data.count = 1;
// → 触发 setter → dep.notify() → Watcher.update()
// → 输出:Render: 1
五、Vue 3 的改进:Proxy + effect
js
// Vue 3 使用 Proxy
const reactiveData = reactive({ count: 0 });
effect(() => {
console.log(reactiveData.count); // 收集依赖
});
reactiveData.count++; // 触发更新
- 优势 :
- 支持动态新增属性;
- 性能更好(无需递归 defineProperty);
- 代码更简洁。
💡 结语
"依赖收集是 Vue 响应式的灵魂。"
角色 | 职责 |
---|---|
defineReactive |
拦截 getter/setter |
Dep |
管理订阅者(Watcher) |
Watcher |
观察数据变化,执行更新 |
过程 | 关键操作 |
---|---|
初始化 | defineReactive |
收集 | Dep.target = watcher + dep.depend() |
更新 | dep.notify() → watcher.update() |
掌握依赖收集机制,你就能:
✅ 理解 Vue 响应式原理;
✅ 调试响应式问题;
✅ 设计自己的响应式系统;
✅ 顺利过渡到 Vue 3 的 reactive
和 effect
。