2026/4/11 14:44:52
网站建设
项目流程
公司邮箱后缀有哪些,合肥网站开发 合肥网站优化,网站开发天津,网站流量共享摘要#xff1a;
本文从零手写一个 Mini Vue 响应式系统#xff0c;逐步还原 reactive、ref、computed、effect 的核心逻辑#xff0c;并结合 Vue 3.5 官方源码#xff08;vue/reactivity#xff09;进行对照分析。你将彻底理解 Proxy 如何拦截数据、依赖如何收集#xf…摘要本文从零手写一个Mini Vue 响应式系统逐步还原reactive、ref、computed、effect的核心逻辑并结合 Vue 3.5 官方源码vue/reactivity进行对照分析。你将彻底理解 Proxy 如何拦截数据、依赖如何收集track、更新如何触发trigger以及为什么ref需要.value。全文包含12 段可运行代码示例、3 张原理图解、5 个常见误区避坑指南助你从“会用”进阶到“精通”。关键词Vue 3响应式系统ProxytracktriggerrefreactivecomputedCSDN一、引言为什么你需要理解响应式原理很多开发者能熟练使用ref和reactive但遇到以下问题时却束手无策为什么直接修改数组索引arr[0] 1不触发更新为什么解构reactive对象会失去响应性为什么ref在模板中自动.value但在 JS 中必须手动写为什么computed是懒执行且带缓存的根本原因你只知其然不知其所以然。本文目标通过手写 Mini Vue源码对照让你真正掌握 Vue 3 响应式系统的设计哲学与实现细节。二、响应式系统的核心思想依赖收集与派发更新Vue 3 响应式基于观察者模式Observer Pattern但用Proxy WeakMap实现了更高效的依赖管理。核心流程三步走Track依赖收集当组件读取某个响应式数据时将其对应的更新函数effect记录下来Trigger派发更新当数据被修改时找出所有依赖它的 effect 并执行Cleanup依赖清理避免内存泄漏移除无效依赖。关键数据结构// target - key - depsSeteffect const targetMap new WeakMapobject, Mapstring | symbol, SetReactiveEffect()三、手把手从零实现 reactive3.1 最简版 reactive仅支持对象// mini-vue/reactive.ts type Target Recordstring, any // 存储依赖关系target - key - effects const targetMap new WeakMapTarget, Mapstring, SetFunction() function track(target: Target, key: string) { 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) } // 当前正在执行的 effect if (activeEffect) { dep.add(activeEffect) } } function trigger(target: Target, key: string) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { dep.forEach(effect effect()) } } let activeEffect: Function | undefined undefined function effect(fn: Function) { activeEffect fn fn() // 立即执行触发 track activeEffect undefined } export function reactiveT extends Target(target: T): T { return new Proxy(target, { get(target, key: string, receiver) { const result Reflect.get(target, key, receiver) // 收集依赖 track(target, key) return result }, set(target, key: string, value, receiver) { const result Reflect.set(target, key, value, receiver) // 派发更新 trigger(target, key) return result } }) }3.2 测试 reactive// test-reactive.ts import { reactive, effect } from ./mini-vue/reactive const state reactive({ count: 0 }) effect(() { console.log(count changed:, state.count) }) state.count // 输出: count changed: 1✅成功数据变化自动触发副作用函数。四、升级支持嵌套对象与数组原版reactive会递归代理所有属性我们来实现// mini-vue/reactive.ts增强版 function createGetter() { return function get(target: Target, key: string, receiver: any) { const result Reflect.get(target, key, receiver) // 递归处理嵌套对象 if (typeof result object result ! null) { return reactive(result) } track(target, key) return result } } function createSetter() { return function set(target: Target, key: string, value: any, receiver: any) { const oldValue target[key] const result Reflect.set(target, key, value, receiver) // 区分新增属性 vs 修改属性 const hadKey Array.isArray(target) ? Number(key) target.length : Object.prototype.hasOwnProperty.call(target, key) if (!hadKey) { // 新增属性trigger ADD trigger(target, key) } else if (value ! oldValue) { // 修改属性trigger SET trigger(target, key) } return result } } export function reactiveT extends Target(target: T): T { // 避免重复代理 if (isReactive(target)) return target return new Proxy(target, { get: createGetter(), set: createSetter() }) } // 判断是否已是响应式对象 export function isReactive(value: unknown): boolean { return !!(value as any).__v_isReactive } // 在 reactive 中标记 export function reactiveT extends Target(target: T): T { if (isReactive(target)) return target const proxy new Proxy(target, { get: createGetter(), set: createSetter() }) // 添加标记 ;(proxy as any).__v_isReactive true return proxy }关键改进递归代理嵌套对象区分ADD/SET触发对数组 length 变化至关重要五、实现 ref包装基本类型reactive无法包装number、string等基本类型于是有了ref。5.1 手写 ref// mini-vue/ref.ts import { track, trigger } from ./reactive class RefImplT { private _value: T public readonly __v_isRef true // 标记为 ref constructor(value: T) { this._value value } get value() { track(this, value) return this._value } set value(newVal: T) { if (newVal ! this._value) { this._value newVal trigger(this, value) } } } export function refT(value: T) { return new RefImpl(value) } // 工具函数判断是否为 ref export function isRef(r: any): r is RefImplany { return !!(r r.__v_isRef true) } // 自动解包 ref用于模板 export function unref(ref: any) { return isRef(ref) ? ref.value : ref }5.2 测试 ref// test-ref.ts import { ref, effect } from ./mini-vue const count ref(0) effect(() { console.log(count:, count.value) }) count.value // 输出: count: 1❓为什么需要.value因为ref是一个对象value是其属性。JS 无法拦截基本类型赋值只能通过对象属性 getter/setter 实现响应式。六、实现 computed懒执行 缓存computed本质是一个带有缓存的 effect。6.1 手写 computed// mini-vue/computed.ts import { effect, track, trigger } from ./reactive import { isFunction } from vue/shared class ComputedRefImplT { public readonly __v_isRef true private _getter: () T private _value: T private _dirty true // 是否需要重新计算 constructor(getter: () T) { this._getter getter } get value() { // 依赖收集 track(this, value) if (this._dirty) { this._value this._getter() this._dirty false } return this._value } // 当依赖变化时标记为 dirty notify() { this._dirty true trigger(this, value) } } export function computedT(getter: () T) { const runner new ComputedRefImpl(getter) // 创建一个 effect当依赖变化时通知 runner effect(() { runner.notify() }, { lazy: true, // 不立即执行 scheduler: () { runner.notify() } }) return runner }⚠️注意上述简化版未处理effect的scheduler完整版需改造effect函数。6.2 升级 effect 支持 scheduler// mini-vue/reactive.ts type EffectOptions { lazy?: boolean scheduler?: () void } export function effect(fn: Function, options: EffectOptions {}) { const _effect () { activeEffect _effect fn() activeEffect undefined } if (!options.lazy) { _effect() } // 保存 scheduler ;(_effect as any).scheduler options.scheduler return _effect } // 修改 trigger function trigger(target: Target, key: string) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { dep.forEach(effectFn { if ((effectFn as any).scheduler) { (effectFn as any).scheduler() } else { effectFn() } }) } }6.3 测试 computed// test-computed.ts import { ref, computed } from ./mini-vue const count ref(1) const double computed(() { console.log(计算 double...) return count.value * 2 }) console.log(double.value) // 输出: 计算 double... \n 2 console.log(double.value) // 输出: 2 缓存生效 count.value 2 console.log(double.value) // 输出: 计算 double... \n 4✅验证成功computed 懒执行、带缓存、依赖变化自动更新。七、Vue 3 官方源码对照vue/reactivity我们来看看 Vue 3.5 的真实实现有何异同。7.1 reactive 源码关键片段// packages/reactivity/src/reactive.ts export function reactive(target: object) { // ... return createReactiveObject( target, false, mutableHandlers, // ← 核心 handler mutableCollectionHandlers, reactiveMap ) } // mutableHandlers export const mutableHandlers: ProxyHandlerobject { get, set, deleteProperty, has, ownKeys }对比我们的实现官方支持更多 trap如deleteProperty、has使用ReactiveFlags处理isReactive标记对数组、Map/Set 有特殊处理7.2 ref 源码关键逻辑// packages/reactivity/src/ref.ts export function ref(value?: unknown) { return createRef(value, false) } function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) }官方优化支持shallowRef浅层响应式对ref(ref(x))做去重处理八、三大核心 API 对比总结特性reactiverefcomputed适用类型对象/数组任意类型衍生值访问方式直接obj.keyref.valuecomputed.value模板中无需.value自动解包自动解包响应式原理Proxy 代理整个对象getter/setter 包装带缓存的 effect性能高批量代理中单属性高缓存✅最佳实践用reactive定义对象状态用ref定义基本类型或需跨组件传递的状态用computed派生计算属性九、5 大常见误区与避坑指南❌ 误区 1解构 reactive 对象会失去响应性// 错误 const { count } reactive({ count: 0 }) effect(() { console.log(count) // 不会响应更新 })原因解构后count是普通 number不再是 Proxy 属性。正确做法// 方案1不解构 const state reactive({ count: 0 }) effect(() console.log(state.count)) // 方案2用 toRefs import { toRefs } from vue const { count } toRefs(reactive({ count: 0 }))❌ 误区 2直接替换整个 reactive 对象let state reactive({ a: 1 }) state reactive({ b: 2 }) // 原组件不会更新原因组件引用的是旧 Proxy 对象。正确做法使用Object.assign或重置属性。❌ 误区 3在 computed 中执行副作用// 危险 const badComputed computed(() { console.log(副作用) // 可能多次执行 return someValue })原则computed 应是纯函数无副作用。❌ 误区 4忘记 ref 的 .valueconst count ref(0) setTimeout(() { count 1 // 类型错误且失去响应性 }, 1000)正确count.value 1❌ 误区 5在非 effect 上下文中读取响应式数据const state reactive({ count: 0 }) console.log(state.count) // 不会收集依赖只有在 effect或 setup 中的模板中读取才会 track。十、实战用 Mini Vue 重构 TodoList我们将用自己实现的响应式系统写一个简单 TodoList。// todo-app.ts import { reactive, effect, ref, computed } from ./mini-vue const todos reactive([ { id: 1, text: 学习 Vue 响应式, done: false } ]) const newTodoText ref() const addTodo () { if (newTodoText.value.trim()) { todos.push({ id: Date.now(), text: newTodoText.value, done: false }) newTodoText.value } } const completedCount computed(() { return todos.filter(t t.done).length }) // 渲染函数模拟组件更新 effect(() { console.clear() console.log( 我的待办 ) todos.forEach(todo { console.log([${todo.done ? ✓ : }] ${todo.text}) }) console.log(已完成: ${completedCount.value}/${todos.length}) console.log(输入新任务回车添加:) }) // 模拟用户输入 addTodo()效果每次调用addTodo()控制台自动刷新列表十一、性能优化避免不必要的 track/triggerVue 3 在以下场景做了优化相同值不 triggerset时比较新旧值只读对象readonly不触发 trackshallowReactive不递归代理嵌套对象WeakMap 自动 GCtarget 被销毁依赖自动清除。启示在大型应用中合理使用shallowRef、markRaw可提升性能。十二、结语响应式不是魔法而是精巧的设计通过手写 Mini Vue我们揭开了 Vue 3 响应式系统的神秘面纱Proxy 是基础但不是全部WeakMap Set 是灵魂实现高效依赖管理effect 是桥梁连接数据与视图ref/computed 是糖衣让 API 更友好。真正的高手既能用好框架也懂其底层逻辑。