Vue依赖收集和派发更新
这是 Vue 响应式系统 的核心概念,Vue2 基于 Object.defineProperty,Vue3 基于 Proxy。我用代码帮你彻底搞懂。
一、整体流程图
plain
┌─────────────┐ 依赖收集 ┌─────────────┐
│ 数据变化 │ ◄──────────────── │ 组件渲染 │
│ (state) │ │ (render) │
└──────┬──────┘ └─────────────┘
│ │
│ 派发更新 │ 读取数据触发getter
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 通知所有 │ │ 建立依赖关系 │
│ 订阅者更新 │ │ (Dep/Watcher)│
│ (notify) │ │ (depend) │
└─────────────┘ └─────────────┘二、Vue2 实现(Object.defineProperty)
javascript
// ========== 1. 依赖收集器 Dep ==========
class Dep {
constructor() {
this.subs = []; // 存储所有Watcher(订阅者)
}
// 添加订阅者
addSub(watcher) {
this.subs.push(watcher);
}
// 依赖收集:当前正在执行的Watcher把自己加进来
depend() {
if (Dep.target) { // Dep.target 是当前正在渲染的Watcher
this.addSub(Dep.target);
Dep.target.addDep(this); // 双向记录,方便后续清理
}
}
// 派发更新:数据变了,通知所有Watcher执行更新
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 全局变量,记录当前正在执行的Watcher
Dep.target = null;
// ========== 2. Watcher(订阅者/观察者) ==========
class Watcher {
constructor(vm, getter, callback) {
this.vm = vm; // Vue实例
this.getter = getter; // 获取数据的函数,如 () => vm.name
this.callback = callback; // 数据变化后的回调
this.deps = []; // 记录自己依赖了哪些Dep
this.get(); // 初始化时立即执行,触发getter进行依赖收集
}
get() {
Dep.target = this; // 把自己设为当前Watcher
const value = this.getter.call(this.vm); // 执行getter,触发数据getter
Dep.target = null; // 收集完毕,清空
return value;
}
addDep(dep) {
this.deps.push(dep);
}
update() {
// 数据变了,重新执行getter获取新值
const newValue = this.getter.call(this.vm);
const oldValue = this.value;
this.value = newValue;
this.callback.call(this.vm, newValue, oldValue);
}
}
// ========== 3. 响应式数据劫持 ==========
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性一个Dep实例
Object.defineProperty(obj, key, {
get() {
// 【依赖收集】:有人读取这个属性时,把当前Watcher加入依赖列表
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 【派发更新】:数据变了,通知所有Watcher更新
dep.notify();
}
});
}
// ========== 4. 完整演示 ==========
const data = { name: 'Vue2', count: 0 };
// 将数据变成响应式
Object.keys(data).forEach(key => defineReactive(data, key, data[key]));
// 模拟组件渲染:创建一个Watcher
const watcher = new Watcher(
data,
() => data.name, // getter:读取name属性
(newVal, oldVal) => { // callback:数据变化后的回调
console.log(`【更新】${oldVal} → ${newVal}`);
}
);
console.log('--- 修改数据,触发更新 ---');
data.name = 'Vue3'; // 触发setter → dep.notify() → watcher.update()
// 输出:【更新】Vue2 → Vue3
data.name = 'Vue3'; // 值没变,不会触发更新
// 无输出三、Vue3 实现(Proxy)
javascript
// ========== Vue3 用 Proxy + Reflect,性能更好,能监听新增/删除属性 ==========
const targetMap = new WeakMap(); // 存储对象 -> (属性 -> Dep) 的映射
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set(); // Vue3用Set存储Watcher,去重更高效
depsMap.set(key, dep);
}
return dep;
}
// 当前正在执行的 effect(类似Vue2的Watcher)
let activeEffect = null;
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 【依赖收集】
if (activeEffect) {
const dep = getDep(target, key);
dep.add(activeEffect);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 【派发更新】
if (oldValue !== value) {
const dep = getDep(target, key);
dep.forEach(effect => effect());
}
return result;
}
});
}
function watchEffect(effectFn) {
activeEffect = effectFn;
effectFn(); // 立即执行,触发getter进行依赖收集
activeEffect = null;
}
// ========== 演示 ==========
const state = reactive({ name: 'Vue3', count: 0 });
watchEffect(() => {
console.log('【渲染】name =', state.name);
});
// 输出:【渲染】name = Vue3 (首次执行,触发getter,建立依赖)
console.log('--- 修改数据 ---');
state.name = 'React';
// 输出:【渲染】name = React (setter触发,通知effect重新执行)
state.count++;
// 无输出,因为effect没有读取count,没有建立依赖关系
// ========== Vue3 优势:自动监听新增属性 ==========
state.age = 25; // Vue2需要Vue.set,Vue3直接支持
watchEffect(() => {
console.log('age =', state.age);
});
// 输出:age = 25
state.age = 26;
// 输出:age = 26四、Vue2 vs Vue3 对比
| 维度 | Vue2 Object.defineProperty | Vue3 Proxy |
|---|---|---|
| 依赖收集时机 | 属性getter被读取时 | Proxy的get陷阱被触发时 |
| 派发更新方式 | 手动遍历Dep.subs数组通知 | 遍历Set自动去重通知 |
| 新增属性 | ❌ 无法监听,需Vue.set | ✅ 自动监听 |
| 删除属性 | ❌ 无法监听,需Vue.delete | ✅ 自动监听 |
| 数组索引 | ❌ 无法直接监听 | ✅ 自动监听 |
| Map/Set | ❌ 不支持 | ✅ 支持 |
| 性能 | 递归遍历所有属性 | 懒代理,按需劫持 |
五、结合你简历的项目应用
javascript
// ========== IRMP报告平台:响应式状态管理 ==========
// Vue3 Pinia 中的依赖收集(底层就是上面的原理)
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
export const useReportStore = defineStore('report', () => {
// ref创建响应式数据 → 底层是reactive({ value: '...' })
const reportList = ref([]); // 依赖收集:组件读取reportList时建立关系
const currentReport = ref(null);
const loading = ref(false);
// computed也是依赖收集的典型应用
const pendingReports = computed(() => {
// 读取reportList.value → 建立computed和reportList的依赖关系
return reportList.value.filter(r => r.status === 'pending');
});
// watch也是基于依赖收集
watch(currentReport, (newVal, oldVal) => {
// currentReport变化时,自动执行回调
console.log('当前报告切换:', oldVal?.id, '→', newVal?.id);
});
async function fetchReports() {
loading.value = true; // 触发setter → 通知所有依赖loading的组件更新
const res = await fetch('/api/reports');
reportList.value = res.data; // 触发setter → 通知pendingReports和用到reportList的组件
loading.value = false;
}
return { reportList, currentReport, loading, pendingReports, fetchReports };
});
// ========== 组件中使用 ==========
<template>
<div>
<!-- 读取loading → 触发getter → 依赖收集 -->
<div v-if="loading">加载中...</div>
<!-- 读取pendingReports → 触发computed getter → 依赖收集 -->
<div v-else>
待审批报告: {{ pendingReports.length }} 条
</div>
</div>
</template>
<script setup>
import { useReportStore } from '@/stores/report';
const { loading, pendingReports, fetchReports } = useReportStore();
// 组件挂载时调用,修改reportList → 派发更新 → 组件重新渲染
onMounted(fetchReports);
</script>六、面试话术
"讲一下Vue的依赖收集和派发更新"
"Vue的响应式系统核心是依赖收集和派发更新两个过程:
依赖收集:组件渲染时会读取数据(触发getter),此时Vue把当前的渲染函数(Watcher)记录到这个数据的依赖列表中。Vue2用
Object.defineProperty劫持getter,Vue3用Proxy的get陷阱。派发更新:当数据被修改时(触发setter),Vue遍历这个数据的依赖列表,通知所有Watcher重新执行,也就是重新渲染组件。
我在IRMP报告平台中用Pinia管理状态,底层就是这套机制。比如
reportList变化时,所有用到它的组件和computed都会自动更新,不需要手动操作DOM。"
"Vue2和Vue3的响应式有什么区别?"
"Vue2用
Object.defineProperty递归劫持已有属性,但无法监听新增属性和数组索引变化,需要Vue.set/Vue.delete。Vue3用
Proxy代理整个对象,可以监听新增、删除、数组索引变化,而且性能更好,是懒代理(用到才劫持)。底层依赖收集从数组改为Set,自动去重更高效。"