摘要 :
本文从零手写一个 Mini Vue 响应式系统 ,逐步还原
reactive、ref、computed、effect的核心逻辑,并结合 Vue 3.5 官方源码(@vue/reactivity)进行对照分析。你将彻底理解 Proxy 如何拦截数据、依赖如何收集(track)、更新如何触发(trigger),以及为什么ref需要.value。全文包含 12 段可运行代码示例 、3 张原理图解 、5 个常见误区避坑指南 ,助你从"会用"进阶到"精通"。
关键词:Vue 3;响应式系统;Proxy;track;trigger;ref;reactive;computed;CSDN
一、引言:为什么你需要理解响应式原理?
很多开发者能熟练使用 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 -> deps(Set<effect>) const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>()
三、手把手:从零实现 reactive
3.1 最简版 reactive(仅支持对象)
// mini-vue/reactive.ts
type Target = Record<string, any>
// 存储依赖关系:target -> key -> effects
const targetMap = new WeakMap<Target, Map<string, Set<Function>>>()
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 reactive<T 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 reactive<T 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 reactive<T 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 RefImpl<T> {
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 ref<T>(value: T) {
return new RefImpl(value)
}
// 工具函数:判断是否为 ref
export function isRef(r: any): r is RefImpl<any> {
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 ComputedRefImpl<T> {
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 computed<T>(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: ProxyHandler<object> = {
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 对比总结
| 特性 | reactive | ref | computed |
|---|---|---|---|
| 适用类型 | 对象/数组 | 任意类型 | 衍生值 |
| 访问方式 | 直接 obj.key |
ref.value |
computed.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 的 .value
const 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/trigger
Vue 3 在以下场景做了优化:
- 相同值不 trigger :
set时比较新旧值; - 只读对象 :
readonly不触发 track; - shallowReactive:不递归代理嵌套对象;
- WeakMap 自动 GC:target 被销毁,依赖自动清除。
💡 启示 :
在大型应用中,合理使用
shallowRef、markRaw可提升性能。
十二、结语:响应式不是魔法,而是精巧的设计
通过手写 Mini Vue,我们揭开了 Vue 3 响应式系统的神秘面纱:
- Proxy 是基础,但不是全部;
- WeakMap + Set 是灵魂,实现高效依赖管理;
- effect 是桥梁,连接数据与视图;
- ref/computed 是糖衣,让 API 更友好。
真正的高手,既能用好框架,也懂其底层逻辑。