Vue3响应式原理:Proxy与依赖收集机制详解

发布时间:2026/7/18 5:14:03
Vue3响应式原理:Proxy与依赖收集机制详解 1. 为什么Vue3选择Proxy作为响应式核心在Vue2时代我们使用Object.defineProperty来实现响应式但这种方式存在几个致命缺陷。首先它无法检测对象属性的添加或删除这就是为什么Vue2中需要特殊API如Vue.set的原因。其次它对数组的变异方法如push、pop等需要特殊处理性能开销较大。最重要的是它需要递归遍历对象的所有属性进行劫持初始化时的性能损耗与数据规模成正比。Proxy是ES6引入的元编程特性它可以在目标对象之前架设一层拦截外界对该对象的访问都必须先通过这层拦截。与Object.defineProperty相比Proxy的优势在于可以拦截多达13种操作包括属性访问、赋值、删除、in操作符等直接代理整个对象不需要递归遍历属性性能更好特别是在大型对象上支持数组索引变化和length修改的检测const target { foo: bar } const handler { get(target, key) { console.log(Getting ${key}) return target[key] } } const proxy new Proxy(target, handler) console.log(proxy.foo) // 输出Getting foo然后输出bar2. Proxy响应式系统核心实现2.1 基础响应式实现我们先从最基础的响应式实现开始。要实现一个响应式系统需要解决三个核心问题如何追踪依赖哪个数据被哪个函数使用如何触发更新数据变化时如何通知依赖如何批量处理更新避免重复计算// 存储当前正在运行的effect let activeEffect: Function | null null // 依赖收集的映射表 const targetMap new WeakMapany, Mapany, SetFunction() function track(target: object, key: unknown) { 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: object, key: unknown) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { dep.forEach(effect effect()) } } function effect(fn: Function) { activeEffect fn fn() activeEffect null }2.2 实现reactive函数基于上述基础我们可以实现Vue3中的reactive函数function reactiveT extends object(target: T): T { const handler: ProxyHandlerT { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { const oldValue Reflect.get(target, key, receiver) const result Reflect.set(target, key, value, receiver) if (oldValue ! value) { trigger(target, key) } return result } } return new Proxy(target, handler) }这个实现已经可以处理基本场景const state reactive({ count: 0 }) effect(() { console.log(Count is: ${state.count}) }) // 立即执行输出Count is: 0 state.count // 触发effect重新执行输出Count is: 12.3 处理嵌套对象和数组上面的基础实现还不能处理嵌套对象和数组我们需要增强它function reactiveT extends object(target: T): T { // 如果已经是代理对象直接返回 if ((target as any).__isReactive) { return target } const handler: ProxyHandlerT { get(target, key, receiver) { // 标记为响应式对象 if (key __isReactive) return true const res Reflect.get(target, key, receiver) track(target, key) // 如果是对象递归代理 if (typeof res object res ! null) { return reactive(res) } return res }, // ...其他拦截器 } return new Proxy(target, handler) }对于数组我们需要特殊处理一些方法const arrayInstrumentations: Recordstring, Function {} ;[push, pop, shift, unshift, splice, sort, reverse].forEach(method { const original Array.prototype[method as any] arrayInstrumentations[method] function(...args: any[]) { const result original.apply(this, args) trigger(this, length) // 数组方法可能改变length return result } }) // 在get拦截器中添加 if (Array.isArray(target) arrayInstrumentations.hasOwnProperty(key)) { return arrayInstrumentations[key as string] }3. Ref实现原理Vue3中的ref用于处理基本类型的响应式它的实现原理其实很简单function refT(value: T) { return { get value() { track(this, value) return value }, set value(newVal) { if (newVal ! value) { value newVal trigger(this, value) } }, __isRef: true } }在模板中使用ref时会自动解包这是通过编译器实现的特殊处理。在reactive对象中访问ref属性也会自动解包// 在reactive的get拦截器中添加 const res Reflect.get(target, key, receiver) if (res?.__isRef) { return res.value }4. 性能优化技巧4.1 避免不必要的响应式转换不是所有对象都需要响应式对于大型不可变数据可以使用shallowRef或markRawfunction markRawT extends object(value: T): T { (value as any).__skip true return value } function reactiveT extends object(target: T): T { if ((target as any).__skip) { return target } // ...原有实现 }4.2 批量更新频繁修改响应式数据会导致多次触发effect可以使用批处理优化let shouldTrack true const queue: Function[] [] function batch(fn: Function) { if (!shouldTrack) { return fn() } shouldTrack false try { fn() // 执行所有排队任务 queue.forEach(job job()) queue.length 0 } finally { shouldTrack true } } function trigger(target: object, key: unknown) { // ...收集依赖 if (!shouldTrack) { // 如果处于批处理中将effect加入队列 dep.forEach(effect queue.push(effect)) return } // 立即执行 dep.forEach(effect effect()) }4.3 懒计算对于计算属性可以实现懒计算和缓存function computedT(getter: () T) { let value: T let dirty true const runner effect(getter, { lazy: true, scheduler: () { dirty true trigger(obj, value) } }) const obj { get value() { if (dirty) { value runner() dirty false track(obj, value) } return value } } return obj }5. 与Vue2响应式系统的对比特性Vue2 (Object.defineProperty)Vue3 (Proxy)检测属性添加/删除需要Vue.set/delete原生支持数组索引变化需要特殊处理原生支持性能递归遍历所有属性按需访问代码复杂度较高较低内存占用较高较低嵌套对象处理初始化时递归代理访问时代理6. 实际应用中的注意事项解构问题解构响应式对象会失去响应性const state reactive({ count: 0 }) // 错误方式 - 失去响应性 let { count } state count // 不会触发更新 // 正确方式 - 使用toRefs const { count } toRefs(state) count.value // 保持响应性循环引用Proxy不能代理已经被代理的对象需要做标记function reactive(target: object) { if ((target as any).__isReactive) { return target } // ...代理逻辑 }原始值访问有时需要访问原始对象function toRawT(observed: T): T { const raw (observed as any).__raw return raw ? toRaw(raw) : observed }异步更新队列Vue会缓冲所有状态变更在下一个tick统一更新import { nextTick } from vue async function increment() { state.count console.log(document.getElementById(count).textContent) // 旧值 await nextTick() console.log(document.getElementById(count).textContent) // 新值 }7. 完整实现示例下面是一个完整的简易响应式库实现type Effect Function { deps?: SetFunction[] } type EffectOptions { lazy?: boolean, scheduler?: Function } let activeEffect: Effect | null null const targetMap new WeakMapany, Mapany, SetEffect() function track(target: object, key: unknown) { 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) activeEffect.deps?.push(dep) } function trigger(target: object, key: unknown) { const depsMap targetMap.get(target) if (!depsMap) return const effects new SetEffect() const dep depsMap.get(key) dep?.forEach(effect effects.add(effect)) effects.forEach(effect { if (effect.scheduler) { effect.scheduler() } else { effect() } }) } function effect(fn: Function, options: EffectOptions {}) { const effectFn: Effect () { cleanup(effectFn) activeEffect effectFn const res fn() activeEffect null return res } effectFn.deps [] effectFn.scheduler options.scheduler if (!options.lazy) { effectFn() } return effectFn } function cleanup(effectFn: Effect) { effectFn.deps?.forEach(dep dep.delete(effectFn)) effectFn.deps [] } function reactiveT extends object(target: T): T { const handler: ProxyHandlerT { get(target, key, receiver) { if (key __isReactive) return true 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 Reflect.get(target, key, receiver) const result Reflect.set(target, key, value, receiver) if (oldValue ! value) { trigger(target, key) } return result }, has(target, key) { track(target, key) return Reflect.has(target, key) }, ownKeys(target) { track(target, iterate) return Reflect.ownKeys(target) }, deleteProperty(target, key) { const hadKey Reflect.has(target, key) const result Reflect.deleteProperty(target, key) if (hadKey) { trigger(target, key) } return result } } return new Proxy(target, handler) } function refT(value: T) { const r { get value() { track(r, value) return value }, set value(newVal) { if (newVal ! value) { value newVal trigger(r, value) } }, __isRef: true } return r } function toRefsT extends object(proxy: T) { const ret: any {} for (const key in proxy) { ret[key] toRef(proxy, key) } return ret } function toRefT extends object, K extends keyof T(proxy: T, key: K) { return { get value() { return proxy[key] }, set value(val) { proxy[key] val }, __isRef: true } }8. 测试我们的响应式系统让我们测试一下这个实现是否满足基本需求// 测试基本响应式 const state reactive({ count: 0, nested: { value: 1 } }) effect(() { console.log(Count: ${state.count}, Nested: ${state.nested.value}) }) // 输出: Count: 0, Nested: 1 state.count // 输出: Count: 1, Nested: 1 state.nested.value // 输出: Count: 1, Nested: 2 // 测试数组 const arr reactive([1, 2, 3]) effect(() { console.log(Array length: ${arr.length}) }) // 输出: Array length: 3 arr.push(4) // 输出: Array length: 4 // 测试ref const num ref(0) effect(() { console.log(Ref value: ${num.value}) }) // 输出: Ref value: 0 num.value // 输出: Ref value: 19. 与Vue3实际实现的差异虽然我们的实现已经涵盖了核心功能但与Vue3的实际实现相比还有以下差异更完善的类型系统Vue3的响应式系统与TypeScript深度集成更高效的依赖收集Vue3使用位运算标记依赖关系更复杂的调度系统支持自定义调度器实现watch、computed等高级功能内存优化对不再使用的effect进行自动清理边缘情况处理处理更多边界条件和特殊场景10. 性能优化实战建议在实际项目中应用响应式系统时可以考虑以下优化策略合理使用shallowRef/shallowReactive对于大型不可变数据const largeList shallowRef([]) // 手动触发更新 largeList.value newList避免在模板中使用复杂表达式计算属性是更好的选择// 不好 template{{ user.firstName user.lastName }} // 好 const fullName computed(() ${user.firstName} ${user.lastName})合理使用markRaw标记不需要响应式的对象const staticData markRaw({ ... })批量更新使用nextTick合并多个状态变更import { nextTick } from vue async function updateMultiple() { state.a 1 state.b 2 await nextTick() // DOM已经更新 }避免深层响应式对于不需要深度监听的对象const shallowState shallowReactive({ nested: { value: 1 // 这个变化不会被追踪 } })11. 响应式系统的扩展应用理解了Proxy响应式原理后我们可以实现更多高级功能状态管理类似Pinia的store系统function defineStore(id: string, setup: () any) { let store: any return () { if (!store) { store reactive(setup()) } return store } }表单验证响应式表单验证系统function useFormValidation(form: any, rules: any) { const errors reactive({}) effect(() { for (const field in rules) { const rule rules[field] errors[field] rule(form[field]) } }) return { errors } }自动持久化自动保存状态到localStoragefunction usePersist(key: string, state: any) { const saved localStorage.getItem(key) if (saved) { Object.assign(state, JSON.parse(saved)) } watch(state, () { localStorage.setItem(key, JSON.stringify(state)) }, { deep: true }) }12. 常见问题与解决方案12.1 为什么我的effect没有触发可能原因访问的属性不在响应式对象中在track之前effect已经执行完毕对象被markRaw标记解决方案// 确保访问的是响应式属性 const state reactive({ count: 0 }) effect(() { console.log(state.count) // 正确 console.log(state.nonExistent) // 不会追踪 }) // 确保effect正确设置 effect(() { // 你的逻辑 })12.2 如何避免无限循环当effect中修改依赖的属性时会导致无限循环const state reactive({ count: 0 }) // 错误示例 - 无限循环 effect(() { state.count state.count 1 })解决方案是使用条件判断或拆分effect// 方案1添加条件 effect(() { if (state.count 10) { state.count } }) // 方案2拆分effect effect(() { console.log(state.count) }) const increment () { state.count }12.3 如何处理异步依赖effect中的异步代码需要特殊处理// 错误示例 - 异步代码不会被追踪 effect(async () { const data await fetchData() state.value data // 这行不会建立响应式联系 }) // 正确方案1拆分effect effect(() { fetchData().then(data { state.value data }) }) // 正确方案2使用watch watch(() fetchData(), data { state.value data })13. 响应式系统的未来演进随着JavaScript语言的发展响应式系统也在不断进化。未来可能会有以下改进方向基于ES Observable的响应式更标准的观察者模式实现编译时响应式通过编译时分析确定依赖关系更细粒度的响应式按需更新组件子树WebAssembly加速对大型数据集的性能优化14. 从零实现的学习价值通过从零实现一个响应式系统你可以获得深入理解Vue3的核心机制掌握Proxy的高级用法学习设计模式在实际框架中的应用提升TypeScript类型编程能力培养框架设计的思维方式15. 进一步学习资源Vue3官方文档 - 响应式系统章节《Vue.js设计与实现》- 霍春阳著ECMAScript Proxy规范Vue3源码中的packages/reactivity模块其他响应式库如MobX、SolidJS的实现原理