Vue 3 响应式原理源码全解析:从 Proxy 到 computed/watch 的完整实现

Vue 3 响应式原理源码全解析:从 Proxy 到 computed/watch 的完整实现 一、写在开头前端框架的核心任务是将数据映射为 DOM并在数据变化时自动更新视图。响应式系统正是实现这一自动更新的基础——当应用状态改变时依赖该状态的视图、计算属性、侦听器等能够自动重新执行。Vue 2 使用Object.defineProperty劫持对象属性的 getter/setter配合观察者模式实现了响应式。但它存在几个难以逾越的局限无法检测属性的添加或删除对数组的变异方法需要额外处理初始化时需递归遍历所有属性存在性能开销。Vue 3 基于 ES2015 的Proxy实现了全新的响应式系统不仅完美解决了上述问题还带来了更好的性能、更简洁的代码结构同时支持 Map、Set 等复杂数据类型。本文将结合packages/reactivity目录下的源码深度剖析这一系统的设计与实现并手写一个 mini 版巩固理解。二、响应式系统的核心Proxy vs Object.defineProperty2.1 Object.defineProperty 的工作原理Object.defineProperty(obj, key, descriptor)可以精确控制对象属性的读取和赋值行为javascriptlet obj {} let val 1 Object.defineProperty(obj, a, { get() { console.log(get a) return val }, set(newVal) { console.log(set a) val newVal } }) obj.a // 触发 get obj.a 2 // 触发 setVue 2 通过递归遍历对象的所有属性为每个属性添加 getter 和 setter。在 getter 中收集依赖Watchersetter 中触发更新。但这种方式存在明显局限无法监听属性新增/删除obj.newKey value不会触发 setter因此 Vue 2 提供了Vue.set/delete。无法直接监听数组索引和 length 变化需要重写数组的 7 种变异方法push/pop/shift/unshift/splice/sort/reverse。初始化时需要深度递归影响启动性能且对于深层嵌套的对象可能造成不必要的观察。简单模拟 Vue 2 响应式javascriptfunction defineReactive(obj, key) { let val obj[key] const dep [] // 依赖列表简化 Object.defineProperty(obj, key, { get() { if (currentEffect) dep.push(currentEffect) return val }, set(newVal) { val newVal dep.forEach(fn fn()) } }) } // 需要递归遍历 function observe(obj) { Object.keys(obj).forEach(key defineReactive(obj, key)) }2.2 Proxy 的工作原理Proxy可以创建一个对象的代理拦截几乎所有基本操作javascriptconst p new Proxy(target, { get(target, key, receiver) { /* ... */ }, set(target, key, value, receiver) { /* ... */ }, deleteProperty(target, key) { /* ... */ }, // 共支持13种拦截方法 })Vue 3 利用 Proxy 的get、set、deleteProperty、has、ownKeys等拦截器能够全面捕获对数据的操作。其优势包括可监听属性的增删set拦截器可感知新增属性deleteProperty拦截删除操作。自然支持数组直接拦截arr[0]、arr.length等操作无需重写数组方法。懒代理只在读取某个属性且其值是对象时才进行递归代理提升初始化性能。可代理更多数据类型Map、Set、WeakMap、WeakSet 等。简单 Proxy 示例javascriptconst handler { get(target, key) { if (typeof target[key] object target[key] ! null) { return new Proxy(target[key], handler) // 懒代理 } return target[key] }, set(target, key, value) { target[key] value console.log(更新视图) return true } } const data new Proxy({ a: 1, arr: [1,2,3] }, handler) data.a 2 // 触发更新 data.b 3 // 触发更新新增属性 data.arr.push(4) // 触发更新2.3 两者的详细对比对比维度Object.definePropertyProxy功能完整性无法监听属性增删、数组索引和 length可拦截13种操作覆盖所有数据变更场景性能初始化需递归遍历内存消耗大懒代理按需递归整体更高效易用性需要额外 APIset/set/delete数组特殊处理统一拦截代码简洁兼容性支持 IE9生态成熟不支持 IE但现代浏览器全面支持数据结构支持仅限普通对象支持数组、Map、Set、WeakMap、WeakSetVue 3 选择 Proxy 正是因为它提供了更完整的拦截能力和更友好的开发体验。虽然放弃了 IE 兼容性但顺应了现代前端发展趋势也使得响应式系统的实现代码量大幅减少。三、Vue 3 响应式系统的整体架构Vue 3 响应式系统的核心模块均位于packages/reactivity/src目录下主要包括reactive将普通对象转换为响应式代理ref包装基本类型值或对象使其变为响应式effect创建副作用函数并在其依赖变化时自动重新执行computed基于其他响应式数据计算得出新值具有懒执行和缓存特性watch/watchEffect侦听响应式数据的变化并执行回调整体工作流程如下text[应用数据] │ ├─ reactive(obj) / ref(val) 创建响应式数据 │ └─ effect / computed / watch 创建副作用依赖响应式数据 │ ▼ Proxy get 拦截 ──── track收集依赖──── 存储到 targetMap (WeakMap) │ Proxy set/delete 拦截 ─── trigger触发依赖──── 取出对应 effect 重新执行 │ ▼ [异步更新队列] - 执行副作用 - 更新 DOM核心概念区分响应式数据被 Proxy 代理后的对象或通过 ref 包装的值能够被“追踪”变化。副作用函数effect依赖响应式数据的函数当数据变化时会被重新调用。组件的渲染函数就是典型的副作用。整个系统围绕“依赖收集”和“触发更新”两个阶段运转接下来我们将深入源码逐一剖析。四、依赖收集的完整实现4.1 依赖收集的基本原理依赖是指某个副作用函数读取了哪些响应式数据。当数据变化时需要通知这些副作用重新执行。依赖收集发生在响应式数据的get 拦截阶段。Vue 3 使用一个三层结构来存储依赖关系texttargetMap: WeakMaptarget, depsMap depsMap: Mapkey, dep dep: SetReactiveEffecttargetMap是一个 WeakMap键为原始目标对象值为depsMap。使用 WeakMap 利于垃圾回收。depsMap是一个 Map键为目标对象的具体属性名值为dep。dep是一个 Set存放所有与该属性关联的ReactiveEffect副作用对象。Set 确保依赖不重复。4.2 核心源码分析reactive 函数packages/reactivity/src/reactive.tsjavascriptexport function reactive(target) { // 如果目标已是只读代理或响应式代理直接返回 if (isReadonly(target)) return target return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap) }createReactiveObject核心逻辑javascriptfunction createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { // 非对象类型直接返回 if (!isObject(target)) return target // 如果已经代理过返回缓存的代理proxyMap 防止重复代理 const existingProxy proxyMap.get(target) if (existingProxy) return existingProxy // 根据类型选择处理器普通对象用 baseHandlers集合类型用 collectionHandlers const proxy new Proxy(target, targetType TargetType.COLLECTION ? collectionHandlers : baseHandlers) proxyMap.set(target, proxy) return proxy }Proxy 的get 拦截器实现在baseHandlers中packages/reactivity/src/baseHandlers.ts核心是调用track收集依赖javascriptfunction createGetter(isReadonly false, shallow false) { return function get(target, key, receiver) { // 处理各种内部 key如 __v_isReactive、__v_raw 等... const res Reflect.get(target, key, receiver) // 非只读时进行依赖收集 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 若值是对象且非浅层代理递归转换为响应式懒代理 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res } }track 函数详细解析packages/reactivity/src/effect.tsjavascriptexport function track(target, type, key) { // 当前激活的 effect 存在时才收集 if (shouldTrack activeEffect) { let depsMap targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap new Map())) } let dep depsMap.get(key) if (!dep) { depsMap.set(key, (dep createDep())) } // 将当前 effect 加入依赖集合 trackEffects(dep) } } function trackEffects(dep) { // 避免重复收集判断 effect 的 deps 数组是否已包含此 dep if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) // effect 也反向记录该 dep便于清理 } }优化策略activeEffect确保只有当前正在执行的副作用才会被收集避免无关读取触发收集。双向记录effect 记录它依赖的所有 depdep 记录所有订阅的 effect使得 effect 重新执行时可以清理过期的依赖。使用位运算trackOpBit进行递归跟踪的控制防止深层嵌套下的重复收集。4.3 ref 的特殊处理ref 用于让基本类型值或对象具备响应式。对象类型 ref 内部会调用reactive转换.value的值。ref 的实现packages/reactivity/src/ref.tsjavascriptexport function ref(value) { return createRef(value, false) } function createRef(rawValue, shallow) { if (isRef(rawValue)) return rawValue return new RefImpl(rawValue, shallow) } class RefImpl { constructor(value, __v_isShallow) { this._rawValue value this._value __v_isShallow ? value : toReactive(value) // 对象转 reactive this.__v_isRef true } get value() { trackRefValue(this) // 收集依赖 return this._value } set value(newVal) { if (hasChanged(this._rawValue, newVal)) { this._rawValue newVal this._value toReactive(newVal) triggerRefValue(this) // 触发更新 } } }ref 依赖收集直接通过trackRefValue将自身作为一个整体收集而不是按属性。其依赖存储在dep属性Set中。与 reactive 相比ref 更轻量适合单一值的响应式包裹且可通过.value直接替换整个对象。五、触发更新的完整实现5.1 触发更新的基本原理触发更新发生在响应式数据的set 或 deleteProperty 拦截阶段。当数据变更时根据修改类型新增、修改、删除从targetMap中找到对应的 dep并执行所有关联的副作用。为提升性能Vue 3 不会立即同步执行副作用而是将副作用放入一个异步更新队列在下一个 tick 统一执行从而批量处理更新避免不必要的重复计算。5.2 核心源码分析set 拦截器packages/reactivity/src/baseHandlers.tsjavascriptfunction createSetter(shallow false) { return function set(target, key, value, receiver) { const oldValue target[key] // 判断操作类型新增ADD还是修改SET const hadKey hasOwn(target, key) const result Reflect.set(target, key, value, receiver) // 若 receiver 是 target 的代理且未被屏蔽触发更新 if (target toRaw(receiver)) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }trigger 函数详细解析packages/reactivity/src/effect.tsjavascriptexport function trigger(target, type, key, newValue, oldValue) { const depsMap targetMap.get(target) if (!depsMap) return let deps [] // 获取与 key 直接相关的依赖 if (key ! undefined) { deps.push(depsMap.get(key)) } // 处理 ADD / DELETE / SET 等特殊操作如数组 length 变更、迭代器相关 switch (type) { case TriggerOpTypes.ADD: // 对数组新增需要触发 length 相关的依赖 if (isArray(target)) { deps.push(depsMap.get(length)) } // ... break case TriggerOpTypes.DELETE: // ... break case TriggerOpTypes.SET: // ... break } // 合并所有需要执行的 effect const effects [] for (const dep of deps) { if (dep) { effects.push(...dep) } } // 通过调度器执行副作用加入更新队列 triggerEffects(createDep(effects)) } function triggerEffects(dep) { for (const effect of dep) { // 若 effect 有 scheduler则使用 scheduler 调度执行 if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }不同操作类型的处理新增属性除了触发该属性的依赖还会触发与迭代如Object.keys相关的依赖删除属性同理对于数组修改length或通过索引设置可能触发多个依赖。5.3 异步更新队列当副作用被触发时不会立即执行effect.run()而是推入一个队列queue并交由scheduler调度。这样同一个 Tick 内的多次数据变更只会触发一次副作用刷新。nextTick的实现packages/runtime-core/src/scheduler.tsjavascriptconst queue [] let isFlushing false const resolvedPromise Promise.resolve() export function queueJob(job) { if (!queue.includes(job)) { queue.push(job) queueFlush() } } function queueFlush() { if (!isFlushing) { isFlushing true resolvedPromise.then(flushJobs) } } function flushJobs() { try { for (let i 0; i queue.length; i) { const job queue[i] job() } } finally { isFlushing false queue.length 0 } }nextTick实际上就是Promise.resolve().then(callback)在 Vue 3 中优先使用微任务等待当前宏任务执行完毕后清空队列。批量更新的优化同一个 effect 若被多次触发只会入队一次通过queue.includes判重。允许提供scheduler例如组件的渲染函数会使用自定义 scheduler确保渲染过程高效批处理。六、computed 的实现原理6.1 computed 的基本概念和特性计算属性接收一个 getter 函数返回一个只读的响应式ref对象。它具有两大核心特性懒执行只有在被读取时才会计算值若没有依赖项变化不会主动重新计算。缓存依赖项不变时多次读取会立即返回上一次的计算结果而不会重复执行 getter。可写计算属性若提供 setter则可通过赋值触发展更新。6.2 核心源码分析computed 函数packages/reactivity/src/computed.tsjavascriptexport function computed(getterOrOptions) { let getter, setter if (isFunction(getterOrOptions)) { getter getterOrOptions setter () warn(Write operation failed: computed value is readonly) } else { getter getterOrOptions.get setter getterOrOptions.set } return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set) }ComputedRefImpl 类javascriptclass ComputedRefImpl { constructor(getter, _setter, isReadonly) { this._setter _setter this._dirty true // 脏标记控制是否需要重新计算 this._cacheable true // 是否启用缓存 this.effect new ReactiveEffect(getter, () { // 当依赖变化时不立即求值仅标记为脏并触发依赖此 computed 的 effect 更新 if (!this._dirty) { this._dirty true triggerRefValue(this) // 通知上层订阅者 } }) } get value() { // 读取时进行依赖收集 const self toRaw(this) trackRefValue(self) // 如果脏或不需要缓存则重新计算 if (self._dirty || !self._cacheable) { self._dirty false self._value self.effect.run() // 执行 getter此时会收集依赖 } return self._value } set value(newValue) { this._setter(newValue) } }懒执行与缓存机制内部创建了一个ReactiveEffect但不立即执行effect 构造时lazy选项阻止自动运行。依赖变更时scheduler 仅将_dirty置为true并触发自身triggerRefValue通知依赖该 computed 的 effect 重新执行。真正求值发生在get value()中且只有_dirty为true时才重新执行 getter否则直接返回_value。当没有任何地方读取 computed 时即使依赖变化也不会发生计算这就是懒执行。6.3 computed 与 effect 的区别对比维度computedeffect执行时机懒执行只有被读取时才计算立即执行一次之后依赖变化时自动重新执行返回值返回一个 ref 对象通过.value访问返回一个 runner 函数无自动返回值概念缓存依赖不变时缓存结果每次重新执行无缓存主要用途派生计算、模板中高效绑定执行副作用如 DOM 更新、watch 回调内部实现基于 effect额外增加了脏检查与缓存纯粹的依赖收集与重新执行使用场景当需要基于现有数据产生新数据并需要缓存时用 computed当需要在数据变化时执行某些操作如异步请求、DOM 操作时用 effect或 watch。七、watch 的实现原理7.1 watch 的基本概念和使用方式Vue 3 提供了watch和watchEffect两个 APIwatchEffect立即执行传入的函数并自动追踪其中的响应式依赖依赖变化时重新执行。watch显式指定监听一个或多个数据源数据变化时触发回调支持懒执行、获取旧值、深度监听等。常用配置immediate: true创建时立即执行回调。deep: true深度监听对象内部变化。flush: pre | post | sync控制回调的执行时机。7.2 核心源码分析watch 函数和watchEffect最终都调用doWatchpackages/reactivity/src/watch.tsjavascriptfunction watch(source, cb, options) { return doWatch(source, cb, options) } export function watchEffect(effect, options) { return doWatch(effect, null, options) }doWatch 核心流程规范化监听源source可以是 ref、reactive 对象、getter 函数、数组等。若为 ref 或 reactive会自动解包或深度追踪。若为函数视为 getter每次运行获取当前值。若为数组组合多个源回调参数为数组形式的新旧值。创建 effect根据配置创建ReactiveEffectscheduler中执行用户回调。javascriptfunction doWatch(source, cb, { immediate, deep, flush } {}) { // ... 规范化 source 为 getter let getter if (isRef(source)) { getter () source.value } else if (isReactive(source)) { getter () source deep true // 默认对 reactive 对象开启深度监听 } else if (isFunction(source)) { getter source } // ... // 深度监听时在 getter 内部递归读取属性确保触发依赖收集 if (deep) { const baseGetter getter getter () traverse(baseGetter()) } let oldValue {} const job () { if (cb) { const newValue effect.run() if (deep || hasChanged(newValue, oldValue)) { cb(newValue, oldValue INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup) oldValue newValue } } else { effect.run() // watchEffect } } let scheduler if (flush sync) { scheduler job } else { scheduler () queuePreFlushCb(job) // 默认 pre 队列DOM 更新前执行 } const effect new ReactiveEffect(getter, scheduler) if (cb) { if (immediate) job() else oldValue effect.run() // 首次执行获取旧值 } else { effect.run() // watchEffect 立即执行 } return () effect.stop() // 返回取消监听的函数 }deep 实现traverse函数递归访问对象的所有属性触发 getter 从而收集所有深层依赖。这样任意层级的变化都能被感知。flush 时机处理pre默认回调在组件更新前执行通过queuePreFlushCb加入 pre 队列。post回调在组件更新后执行通过queuePostRenderEffect。sync同步执行数据变化立即触发回调。7.3 watch 与 computed 的区别对比维度watchcomputed用途执行副作用异步请求、DOM 操作派生计算新值主要用于模板绑定执行默认懒执行immediate 可改为立即懒执行依赖不变不计算返回值返回停止监听函数返回只读 ref缓存无有访问新值回调参数提供新旧值通过.value获取当前计算结果深度监听支持 deep 递归追踪依赖自动追踪无需 deep典型场景“当某个数据变化时发起网络请求”“根据 firstName 和 lastName 计算 fullName”两者在响应式系统中分工明确computed 用于纯计算watch 用于有副作用的响应。八、手写一个简化版的 Vue 3 响应式系统我们将从零实现一个 mini 响应式系统包括reactive、effect、computed、watch。8.1 整体设计思路核心模块reactive(target)返回 Proxy 代理。effect(fn)立刻执行 fn并追踪依赖。track(target, key)在 get 中调用收集当前 effect。trigger(target, key)在 set 中调用运行对应的 effect。computed(getter)返回懒执行、有缓存的 ref。watch(source, cb)监听 source 变化并执行 cb。数据结构targetMapWeakMap -depsMapMap -depSet。8.2 分步实现javascript// mini-reactive.js let activeEffect null const targetMap new WeakMap() // ---- track trigger ---- function track(target, key) { 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())) } dep.add(activeEffect) activeEffect.deps.push(dep) // 反向记录 } function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { // 使用新 Set 避免死循环清理等 const effectsToRun new Set(dep) effectsToRun.forEach(effect { if (effect.scheduler) { effect.scheduler() } else { effect.run() } }) } } // ---- reactive ---- function reactive(target) { return new Proxy(target, { get(target, key, receiver) { const res Reflect.get(target, key, receiver) track(target, key) if (typeof res object res ! null) { return reactive(res) // 懒代理 } return res }, set(target, key, value, receiver) { const oldValue target[key] const result Reflect.set(target, key, value, receiver) if (oldValue ! value) { trigger(target, key) } return result } }) } // ---- effect ---- class ReactiveEffect { constructor(fn, scheduler null) { this.fn fn this.scheduler scheduler this.deps [] } run() { // 清理上次依赖重新收集 cleanupEffect(this) activeEffect this const result this.fn() activeEffect null return result } stop() { cleanupEffect(this) this.active false } } function cleanupEffect(effect) { effect.deps.forEach(dep dep.delete(effect)) effect.deps.length 0 } function effect(fn, options {}) { const _effect new ReactiveEffect(fn, options.scheduler) _effect.run() const runner _effect.run.bind(_effect) runner.effect _effect return runner } // ---- ref ---- function ref(value) { return new RefImpl(value) } class RefImpl { constructor(value) { this._value value this.dep new Set() } get value() { if (activeEffect) { this.dep.add(activeEffect) } return this._value } set value(newVal) { if (newVal ! this._value) { this._value newVal this.dep.forEach(effect { if (effect.scheduler) effect.scheduler() else effect.run() }) } } } // ---- computed ---- function computed(getter) { let dirty true let value const _effect new ReactiveEffect(getter, () { // scheduler依赖变化时标记脏不立即求值 if (!dirty) { dirty true trigger(computedRef, value) } }) const computedRef { get value() { track(computedRef, value) // 收集 computed 的依赖 if (dirty) { dirty false value _effect.run() } return value } } return computedRef } // ---- watch ---- function watch(source, cb, options {}) { let getter if (typeof source function) { getter source } else if (source instanceof RefImpl) { getter () source.value } else { // 简化假设为 reactive 对象深度监听 getter () traverse(source) } let oldValue const job () { const newValue effect.run() cb(newValue, oldValue) oldValue newValue } const effect new ReactiveEffect(getter, job) if (options.immediate) { job() } else { oldValue effect.run() } } // 深度遍历依赖 function traverse(value, seen new Set()) { if (typeof value ! object || value null || seen.has(value)) return value seen.add(value) for (const key in value) { traverse(value[key], seen) } return value }8.3 测试与验证javascript// 测试 reactive 和 effect const state reactive({ count: 0, nested: { a: 1 } }) let dummy effect(() { dummy state.count * 2 }) console.log(dummy) // 0 state.count 5 console.log(dummy) // 10 // 测试 computed const countRef ref(2) const double computed(() countRef.value * 2) console.log(double.value) // 4 countRef.value 3 console.log(double.value) // 6懒计算 // 测试 watch let oldVal, newVal watch(() state.count, (n, o) { newVal n; oldVal o }) state.count 8 console.log(oldVal, newVal) // 5 8与官方实现的差异未处理数组 length 与迭代器相关依赖、Map/Set 的拦截。没有异步更新队列同步执行官方使用微任务批处理。未实现 effect 调度优先级、stop时的完全清理。缺少边界情况处理如代理只读对象、避免死循环等。完整可运行代码及在线示例请查看CodeSandBox 链接读者可自行创建运行环境测试。九、总结与思考Vue 3 响应式系统基于 Proxy 重新设计核心思想仍然是依赖收集与触发更新但实现上更加精简、高效。通过WeakMap - Map - Set三层结构管理依赖通过懒代理提升初始化性能通过effect统一管理副作用通过computed和watch提供更上层的响应式工具。本文核心回顾Proxy 对比 Object.defineProperty 的四大优势。reactive / ref 的创建与依赖收集流程。track / trigger 的源码实现与数据结构。computed 的懒执行、缓存与 effect 的关系。watch 的多源监听、深度追踪与回调调度。优势与不足优势支持更多数据结构、性能更佳、代码更简洁、无 set/set/delete 等补丁 API。不足不支持 IE依赖 Proxy 导致 Polyfill 困难深层嵌套大对象频繁操作时仍有优化空间。展望未来响应式系统可能会引入更细粒度的调度、与编译时优化如 Vue Vapor Mode深度结合甚至利用 TC39 新特性如 Signals 提案进一步统一状态管理。本文通过对 Vue 3 响应式源码的逐层剖析从设计思想到核心实现再到手写 mini 系统力求帮助读者建立起完整的知识图谱。欢迎在评论区分享你对响应式系统的理解或疑问我们一起探讨