《Vue3 从入门到大神25篇》手撕 Vue3 响应式原理 —— 手写 Mini-Vue

《Vue3 从入门到大神25篇》手撕 Vue3 响应式原理 —— 手写 Mini-Vue
前言前面的文章中我们已经反复提到Vue3 的响应式基于Proxy Reflect。你也知道了reactive创建响应式对象effect是副作用函数track收集依赖trigger触发更新但问题是这些东西在源码里到底是怎么串起来的这一篇我们从零手写一个Mini-Vue 响应式系统只用一个文件、不到 100 行代码就把 Vue3 响应式的核心链路跑通。写完之后你会发现原来 Vue3 的响应式并没有想象中那么神秘。一、目标我们要实现什么最终效果const state reactive({ count: 0 }) effect(() { console.log(count:, state.count) }) state.count // 控制台自动打印最新值✅ 数据变化 → 自动重新执行副作用函数这就是 Vue3 响应式的最小可用模型。二、核心设计思路┌─────────────────────────────────────────────┐ │ 响应式系统 │ ├─────────────────────────────────────────────┤ │ │ │ reactive(obj) ──→ Proxy 代理对象 │ │ │ │ │ ▼ │ │ 读取属性 (get) ──→ track() 收集依赖 │ │ │ │ │ ▼ │ │ 修改属性 (set) ──→ trigger() 触发更新 │ │ │ │ │ ▼ │ │ effect() 重新执行 │ │ │ └─────────────────────────────────────────────┘三、数据结构设计1️⃣ 依赖存储结构WeakMap (target → Map (key → Set (effects)))// 存储所有依赖关系 const targetMap new WeakMap()为什么用这三层层级类型作用WeakMaptarget → ...每个响应式对象一个 MapMapkey → ...每个属性一个 SetSeteffects该属性的所有副作用函数WeakMap 的好处当响应式对象被垃圾回收时依赖关系自动清除不会内存泄漏。四、实现 track依赖收集// 当前正在执行的 effect let activeEffect null function track(target, key) { if (!activeEffect) return // 获取 target 对应的 Map let depsMap targetMap.get(target) if (!depsMap) { depsMap new Map() targetMap.set(target, depsMap) } // 获取 key 对应的 Set let dep depsMap.get(key) if (!dep) { dep new Set() depsMap.set(key, dep) } // 将当前 effect 加入 Set dep.add(activeEffect) }核心逻辑把当前正在运行的副作用函数和被读取的属性建立关联。五、实现 trigger触发更新function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const effects depsMap.get(key) if (!effects) return // 依次执行所有副作用函数 effects.forEach(effect effect()) }核心逻辑找到和这个属性关联的所有副作用函数全部重新执行。六、实现 reactive响应式对象function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { // 依赖收集 track(target, key) // 返回属性值 return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { // 设置属性值 const result Reflect.set(target, key, value, receiver) // 触发更新 trigger(target, key) return result } }) }这就是 Vue3 reactive 的最小实现get 中收集依赖set 中触发更新Reflect 保证正确的 this 指向七、实现 effect副作用函数function effect(fn) { // 包装成可调度函数 const effectFn () { activeEffect effectFn fn() activeEffect null } // 立即执行一次 effectFn() return effectFn }关键点把当前 effect 设为activeEffect执行用户传入的函数此时会触发 get → track执行完毕后清空activeEffect八、完整代码不到 60 行// Mini-Vue 响应式核心 const targetMap new WeakMap() let activeEffect null function track(target, key) { if (!activeEffect) return let depsMap targetMap.get(target) if (!depsMap) { depsMap new Map() targetMap.set(target, depsMap) } let dep depsMap.get(key) if (!dep) { dep new Set() depsMap.set(key, dep) } dep.add(activeEffect) } function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const effects depsMap.get(key) effects?.forEach(fn fn()) } function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { const result Reflect.set(target, key, value, receiver) trigger(target, key) return result } }) } function effect(fn) { const effectFn () { activeEffect effectFn fn() activeEffect null } effectFn() return effectFn }九、验证跑一下试试const state reactive({ count: 0, name: Vue3 }) effect(() { console.log(count is: ${state.count}) }) // 输出: count is: 0 state.count // 输出: count is: 1 state.count 10 // 输出: count is: 10✅完美运行十、对照 Vue3 源码我们的实现Vue3 源码track()packages/reactivity/src/effect.ts中的tracktrigger()同文件中的triggerreactive()packages/reactivity/src/reactive.tseffect()packages/reactivity/src/effect.tsVue3 源码更复杂处理了数组、Map/Set、嵌套对象等但核心思想和我们写的一模一样。十一、局限性说明诚实交代我们的 Mini-Vue 还不能处理场景说明嵌套对象需要递归代理Vue3 用Reflect.get 懒代理数组需要特殊处理length和索引Map / Set需要重写方法多次触发需要 effect 栈防止嵌套调度执行需要 scheduler 控制执行时机这些会在后续文章中逐步完善。十二、面试高频问答Q1手写 Vue3 响应式的核心步骤用 WeakMap 存储依赖关系Proxy get 中 track 收集依赖Proxy set 中 trigger 触发更新effect 包装函数并立即执行Q2为什么用 WeakMap 而不是 MapWeakMap 的键是弱引用不影响垃圾回收防止内存泄漏。Q3Reflect 在这里的作用保证正确的 this 指向统一操作 API。十三、总结源码级Vue3 响应式的本质是Proxy 依赖收集 触发更新核心数据结构是WeakMap → Map → Settrack建立关联trigger触发执行手写一遍比看十遍源码都有用 下期预告第 26 篇Virtual DOM 与 Diff 算法 —— Vue3 的高效更新机制