Vue 的响应式系统中,依赖收集和派发更新
文章目录
在 Vue 的响应式系统中,依赖收集和派发更新是实现数据驱动视图的核心机制。它们确保了当数据发生变化时,依赖该数据的视图或逻辑能自动更新,无需手动操作 DOM。下面详细说明这两个过程的实现原理和流程:
一、依赖收集(Dependency Collection)
定义:当响应式数据被访问时,Vue 会记录下“谁在使用这个数据”(即依赖),这个过程称为依赖收集。
核心角色
-
Dep(依赖管理器):
每个响应式数据(或属性)都会对应一个Dep实例,用于存储所有依赖该数据的观察者(Watcher)。- 主要方法:
addSub(watcher)(添加依赖)、notify()(通知所有依赖更新)。
- 主要方法:
-
Watcher(观察者):
代表一个“依赖”,通常对应一个组件的渲染逻辑或用户定义的watch回调。当数据变化时,Watcher会触发更新(如重新渲染组件)。
收集流程(以 Vue 2 为例)
-
初始化响应式数据:
Vue 初始化时,会通过Object.defineProperty对data中的属性进行劫持,重写getter和setter。 -
组件渲染触发依赖收集:
当组件首次渲染时,会执行其渲染函数(render),过程中会读取data中的属性(如this.msg),此时会触发属性的getter。 -
getter中的依赖收集逻辑:// 简化版 getter 逻辑 Object.defineProperty(data, 'msg', { get() { // 核心:将当前活跃的 Watcher 添加到 Dep 中 if (Dep.target) { dep.addSub(Dep.target); // dep 是当前属性的 Dep 实例 } return value; }, // ...setter 逻辑 });Dep.target:全局变量,指向当前正在执行的Watcher(如组件渲染时的Watcher)。
-
完成依赖记录:
Dep会将Watcher存入自身的依赖列表(subs),至此,“数据→Watcher”的映射关系建立完成。
为什么需要依赖收集?
- 避免“无关更新”:只更新依赖变化数据的组件,而非所有组件。例如,
msg变化时,只重新渲染使用了msg的组件。 - 实现精准更新:确保数据变化时,所有依赖它的地方(模板、
computed、watch等)都能被触发。
二、派发更新(Dispatch Update)
定义:当响应式数据发生变化时,Vue 会通知所有依赖该数据的 Watcher 执行更新逻辑,这个过程称为派发更新。
触发时机
当通过 this.msg = 'new value' 修改数据时,会触发属性的 setter,进而启动派发更新流程。
派发流程(以 Vue 2 为例)
-
setter触发更新通知:// 简化版 setter 逻辑 Object.defineProperty(data, 'msg', { set(newValue) { if (newValue !== value) { value = newValue; dep.notify(); // 通知所有依赖的 Watcher 更新 } }, // ...getter 逻辑 }); -
Dep通知所有Watcher:
dep.notify()会遍历自身的subs列表(所有依赖该数据的Watcher),并调用每个Watcher的update()方法。 -
Watcher执行更新:Watcher.update()不会立即执行更新,而是先将自身放入一个队列(异步更新队列),避免同一轮事件循环中多次更新同一Watcher。- 队列会在本轮事件循环的微任务阶段(通过
nextTick)统一执行,确保 DOM 只更新一次,提升性能。
-
执行更新逻辑:
- 对于组件渲染的
Watcher:会调用组件的render函数重新生成虚拟 DOM,再通过 Diff 算法更新真实 DOM。 - 对于用户定义的
watch:会执行对应的回调函数。
- 对于组件渲染的
异步更新队列的意义
- 避免重复计算:例如,连续修改
this.msg = 'a'和this.msg = 'b',只会触发一次更新(最终使用'b')。 - 提升性能:批量处理所有更新,减少 DOM 操作次数(DOM 操作是性能瓶颈)。
三、Vue 3 中的改进
Vue 3 改用 Proxy 实现响应式,依赖收集和派发更新的核心思想不变,但流程更简洁:
- 依赖收集:通过
Proxy的get陷阱收集依赖,无需递归劫持对象属性(Proxy能代理整个对象)。 - 派发更新:通过
Proxy的set/deleteProperty陷阱触发更新,支持更多场景(如数组索引修改、新增属性)。 Watcher改名:Vue 3 中Watcher更名为Effect(副作用),但作用类似。
四、代码执行流程解析
4-1. 初始化阶段
- 创建
Vue实例时,observe函数会递归处理data中的所有属性 - 每个属性通过
defineReactive被转为响应式,即重写getter和setter - 同时创建
Watcher实例,关联render函数(视图渲染逻辑)
4-2. 依赖收集过程
- 当
Watcher初始化时,会执行get()方法 Dep.target被设置为当前Watcher(全局唯一)- 执行
render函数,读取this.data.msg,触发msg属性的getter getter中检查到Dep.target存在,将当前Watcher添加到msg属性对应的Dep实例中- 依赖收集完成后,
Dep.target被重置为null
4-3. 派发更新过程
- 当修改
vm.data.msg = 'Hello Reactivity'时,触发msg属性的setter setter中调用dep.notify(),通知所有依赖的WatcherDep遍历subs列表,调用每个Watcher的update()方法Watcher执行updateFn(即render函数),重新渲染视图
五、关键输出解析
运行代码后,控制台会输出以下内容,清晰展示整个流程:
读取属性 msg: Hello Vue
--- 执行视图渲染 ---
修改属性 msg: Hello Reactivity
--- 开始派发更新 ---
Watcher 执行更新
--- 执行视图渲染 ---
读取属性 user: [object Object]
读取属性 name: 张三
修改属性 name: 李四
--- 开始派发更新 ---
Watcher 执行更新
--- 执行视图渲染 ---
读取属性 msg: Hello Reactivity
修改属性 msg: Hello Again
--- 开始派发更新 ---
Watcher 执行更新
--- 执行视图渲染 ---
读取属性 msg: Hello Again
六、Vue 实际实现与模拟代码的差异
- 嵌套对象处理:实际 Vue 中会递归观察所有嵌套对象,本示例简化实现了这一点
- 数组支持:Vue 对数组方法进行了特殊处理(如
push、splice),本示例未包含 - 异步更新:真实 Vue 中更新会通过
nextTick异步执行,避免频繁 DOM 操作 - 多类型 Watcher:实际 Vue 中有渲染 Watcher、计算属性 Watcher、用户 Watcher 等多种类型
七、代码如下:
// 1. 依赖管理器(Dep):管理每个响应式数据的依赖列表
class Dep {
constructor() {
this.subs = []; // 存储所有依赖(Watcher)
}
// 添加依赖
addSub(watcher) {
if (watcher && !this.subs.includes(watcher)) {
this.subs.push(watcher);
}
}
// 通知所有依赖更新
notify() {
console.log('--- 开始派发更新 ---');
this.subs.forEach(watcher => {
watcher.update(); // 调用每个Watcher的更新方法
});
}
}
// 2. 观察者(Watcher):代表一个依赖,数据变化时执行更新
class Watcher {
/**
* @param {Object} vm - 组件实例
* @param {Function} updateFn - 数据变化时执行的回调
*/
constructor(vm, updateFn) {
this.vm = vm;
this.updateFn = updateFn;
// 初始化时立即执行一次,触发依赖收集
this.get();
}
// 触发依赖收集
get() {
Dep.target = this; // 将当前Watcher设为全局目标
this.updateFn.call(this.vm); // 执行更新函数(会读取响应式数据)
Dep.target = null; // 重置全局目标
}
// 执行更新
update() {
console.log('Watcher 执行更新');
this.updateFn.call(this.vm); // 重新执行更新函数
}
}
// 3. 响应式处理函数:将对象转为响应式
function defineReactive(obj, key, value) {
// 递归处理嵌套对象
observe(value);
// 每个属性对应一个Dep实例
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
console.log(`读取属性 ${key}: ${value}`);
// 依赖收集:如果当前有活跃的Watcher,就添加到Dep中
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newValue) {
if (newValue !== value) {
console.log(`修改属性 ${key}: ${newValue}`);
value = newValue;
observe(newValue); // 新值如果是对象,也需要转为响应式
dep.notify(); // 通知所有依赖更新
}
}
});
}
// 4. 递归处理对象的所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
// 遍历对象的所有属性,转为响应式
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 5. 模拟Vue组件实例
class Vue {
constructor(options) {
this.data = options.data;
this.el = options.el;
// 将data转为响应式
observe(this.data);
// 创建Watcher,关联更新函数(模拟视图渲染)
new Watcher(this, () => this.render());
}
// 模拟视图渲染
render() {
console.log('--- 执行视图渲染 ---');
// 这里读取响应式数据,会触发getter,进行依赖收集
document.querySelector(this.el).textContent = this.data.msg;
}
}
// 6. 测试代码
// 初始化实例
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
user: {
name: '张三'
}
}
});
// 模拟数据更新,观察控制台输出
setTimeout(() => {
vm.data.msg = 'Hello Reactivity'; // 修改基本类型
}, 1000);
setTimeout(() => {
vm.data.user.name = '李四'; // 修改嵌套对象
}, 2000);
setTimeout(() => {
vm.data.msg = 'Hello Again'; // 再次修改
}, 3000);
八、总结
- 依赖收集:通过
getter记录“谁用到了数据”,建立数据→依赖(Watcher)的映射。 - 派发更新:通过
setter触发Dep通知所有依赖,通过异步队列批量执行更新,最终实现视图与数据同步。
通过这个简化模型,可以清晰地理解 Vue 响应式系统的核心:数据变化自动触发依赖更新,这也是 Vue 实现数据驱动视图的基础。
这两个过程共同构成了 Vue 响应式系统的核心,也是“数据驱动”理念的具体实现。理解它们有助于解决响应式相,关的 bugs(如数据更新后视图不刷新),并写出更高效的代码。

847

被折叠的 条评论
为什么被折叠?



