公司动态
《Vue3 从入门到大神35篇》Vue3 源码详解(五):effect 依赖收集原理——track 与 trigger 是如何工作的?
本篇关键词effect、effectStack、Dep、cleanup、依赖清理、嵌套 effect、调度雏形难度等级⭐⭐⭐⭐⭐工程价值这是整个 Vue3 响应式系统的“心脏”。读懂这一篇你不仅能彻底理解 computed、watch、组件更新的本质还能从根本上规避“依赖残留”“无限循环”等高级 Bug。一、开篇为什么 effect 是响应式的灵魂在 Vue3 中一切副作用都源于effectcomputed→ 特殊的effectwatch/watchEffect→ 包装后的effect组件渲染 → 一个巨大的componentEffect一句话定义effect 是“可被重新执行的副作用函数”是数据与视图之间的桥梁。二、回顾依赖收集的三角关系我们在第三篇手写过这个结构target → key → dep → effect但在 Vue3 源码中这个关系更加严谨WeakMaptarget, Mapkey, Dep Dep SetReactiveEffect而effect本身并不只是一个函数而是一个对象。三、ReactiveEffecteffect 的真实身份在packages/reactivity/src/effect.ts中核心类是class ReactiveEffectT any { active true deps: Dep[] [] // 记录我依赖了哪些 Dep parent: ReactiveEffect | null null constructor( public fn: () T, public scheduler?: EffectScheduler ) {} run() { // 核心逻辑 } }为什么 deps 是一个数组因为一个effect可能依赖多个响应式属性effect(() { console.log(state.a state.b) })deps同时包含a和b对应的Dep。四、effectStack解决嵌套 effect 的关键1️⃣ 为什么会有嵌套 effecteffect(() { console.log(parent) effect(() { console.log(child) }) })Vue 组件渲染本身就是一个嵌套结构App.vue ├─ CompA.vue │ └─ CompB.vue每一层渲染都是一个effect。2️⃣ effectStack 的实现let activeEffect: ReactiveEffect | null null const effectStack: ReactiveEffect[] [] class ReactiveEffect { run() { if (!this.active) return this.fn() if (!effectStack.includes(this)) { try { effectStack.push((activeEffect this)) return this.fn() } finally { effectStack.pop() activeEffect effectStack[effectStack.length - 1] || null } } } }✅关键点activeEffect永远指向“当前正在执行的 effect”finally确保即使报错也能恢复栈这就是 Vue3 能正确处理嵌套渲染的根本原因。五、track依赖收集的完整流程1️⃣ track 的触发时机// reactive get 拦截中 get(target, key) { track(target, TrackOpTypes.GET, key) return Reflect.get(...) }2️⃣ track 源码精读export function track(target: object, type: TrackOpTypes, key: unknown) { if (!activeEffect) return let depsMap targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap new Map())) } let dep depsMap.get(key) if (!dep) { depsMap.set(key, (dep new Set())) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }重点拆解✅双向记录Dep记录effecteffect.deps记录Dep为什么要双向记录为了后续清理依赖。六、trigger依赖触发的完整流程1️⃣ trigger 的触发时机set(target, key, value) { trigger(target, TriggerOpTypes.SET, key) return Reflect.set(...) }2️⃣ trigger 源码精读简化版export function trigger(target: object, type: TriggerOpTypes, key: unknown) { const depsMap targetMap.get(target) if (!depsMap) return const effects new SetReactiveEffect() const add (effectsToAdd?: Dep) { effectsToAdd?.forEach(effect effects.add(effect)) } add(depsMap.get(key)) effects.forEach(effect { if (effect.scheduler) { effect.scheduler() } else { effect.run() } }) }✅核心逻辑找到 key 对应的所有 effect如果有 scheduler → 交给调度器否则 → 立即执行这就是 computed、watch、组件更新的统一入口。七、依赖清理cleanupVue3 相比 Vue2 的巨大进步1️⃣ Vue2 的经典 Bugif (state.show) { console.log(state.msg) }当state.show从true→falseVue2仍然会收集state.msg的依赖造成依赖残留2️⃣ Vue3 的解决方案cleanupEffectfunction cleanupEffect(effect: ReactiveEffect) { const { deps } effect for (let i 0; i deps.length; i) { deps[i].delete(effect) } effect.deps.length 0 }在effect.run()之前调用run() { cleanupEffect(this) effectStack.push((activeEffect this)) try { return this.fn() } finally { effectStack.pop() activeEffect effectStack[effectStack.length - 1] || null } }✅效果每次执行前清空旧依赖执行后重新收集新依赖完美适配if / switch / 三元表达式这是 Vue3 响应式系统“自愈能力”的来源。八、调度器雏形为什么 Vue3 不会频繁更新1️⃣ 问题场景state.count state.count state.count你不希望触发三次更新。2️⃣ scheduler 的作用class ReactiveEffect { constructor( public fn: () T, public scheduler?: EffectScheduler ) {} } run() { if (this.scheduler) { this.scheduler(this) } else { this.fn() } }组件渲染 effect 的 scheduler将更新推入微任务队列合并多次更新实现nextTick下一篇讲 watch / computed 时会深入九、调试实战用断点验证全流程在 VSCode 中依次在这些位置打断点packages/reactivity/src/effect.ts ├── ReactiveEffect.run() ├── track() ├── trigger() └── cleanupEffect()然后运行const state reactive({ ok: true, text: hello }) effect(() { console.log(state.ok ? state.text : no) }) state.ok false state.text world观察cleanupEffect是否被调用effect.deps如何变化trigger中effects的数量你会发现Vue3 的依赖系统是“动态重构”的。十、面试追问为什么 effect.deps 是数组而不是 Set因为同一个 Dep 可能被多个 key 共享数组更利于 cleanup。Vue3 如何避免死循环activeEffect在执行期间不会被重复加入 Dep。computed 和 effect 的区别computed 是懒执行 缓存 脏检查的 effect。为什么 cleanup 要在 run 之前防止旧依赖污染新依赖避免内存泄漏。十一、本篇总结✅effect不是函数而是对象✅effectStack解决嵌套执行问题✅track双向记录为 cleanup 做准备✅cleanupEffect是 Vue3 响应式“自愈”的关键✅scheduler是性能优化的入口一句话升华Vue3 的响应式系统不是“收集一次依赖就结束”而是每一次执行都是一次依赖的重建。十二、下一篇预告《Vue3 源码详解六computed 源码分析——懒计算与缓存机制的实现》