主题
实现响应式基础 - reactive
本节对标 Vue 3 源码
@vue/reactivity中的reactive.ts
什么是响应式?
响应式的本质是:当数据变化时,自动执行依赖这些数据的副作用函数。
Vue 2 使用 Object.defineProperty 实现响应式,存在以下局限:
- 无法检测属性的添加和删除(需要
Vue.set/Vue.delete) - 无法检测数组索引和 length 的变化
- 需要递归遍历对象的所有属性
Vue 3 使用 Proxy 重写了整个响应式系统,从根本上解决了这些问题。
核心数据结构
Vue 3 响应式的依赖收集基于一个三层 Map 结构:
targetMap: WeakMap<target, depsMap>
└── depsMap: Map<key, dep>
└── dep: Set<ReactiveEffect>- targetMap:以原始对象为 key,存储该对象所有属性的依赖映射
- depsMap:以属性名为 key,存储该属性的所有依赖
- dep:一个 Set,存储所有依赖该属性的 effect 函数
用 WeakMap 是因为当原始对象被垃圾回收时,对应的依赖映射也会自动清理。
ts
// 依赖集合类型:存储所有依赖某个属性的 effect 函数
type Dep = Set<ReactiveEffect>
// 属性到依赖集合的映射类型:一个对象的每个属性都有自己的依赖集合
type KeyToDepMap = Map<any, Dep>
// 全局依赖映射表:使用 WeakMap 以原始对象为 key,当对象被回收时对应的依赖映射也会被自动清理,避免内存泄漏
const targetMap = new WeakMap<any, KeyToDepMap>()实现 reactive
reactive 的核心是使用 Proxy 包装原始对象,拦截 get 和 set 操作:
ts
// 缓存已创建的 proxy,用 WeakMap 确保同一对象只创建一个代理,且不影响垃圾回收
const reactiveMap = new WeakMap<object, any>()
export function reactive<T extends object>(target: T): T {
// 先检查该对象是否已经有对应的 proxy,如果有则直接返回,避免重复代理
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
return existingProxy
}
// 使用 Proxy 包装原始对象,拦截属性的读取、设置和删除操作
const proxy = new Proxy(target, {
// get 拦截器:在读取属性时触发依赖收集
get(target, key, receiver) {
// 当访问 __v_isReactive 标识时,返回 true,用于 isReactive() 判断
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
// 当访问 __v_raw 标识时,返回原始对象,用于 toRaw() 获取原始值
if (key === ReactiveFlags.RAW) {
return target
}
// 使用 Reflect.get 读取属性值,传递 receiver 确保 getter 中 this 指向代理对象
const res = Reflect.get(target, key, receiver)
// 依赖收集:将当前正在执行的 effect 添加到该属性的依赖集合中
track(target, key)
// 如果属性值是对象,递归转换为响应式(惰性转换:只在访问时才转换,而非初始化时遍历所有属性)
if (isObject(res)) {
return reactive(res)
}
return res
},
// set 拦截器:在设置属性时触发依赖更新
set(target, key, value, receiver) {
// 记录旧值,用于后续比较判断是否需要触发更新
const oldValue = (target as any)[key]
// 使用 Reflect.set 设置属性值
const result = Reflect.set(target, key, value, receiver)
// 只有在值真正发生变化时才触发更新,避免不必要的重复渲染
if (oldValue !== value) {
// 触发更新:通知所有依赖该属性的 effect 重新执行
trigger(target, key)
}
return result
},
// deleteProperty 拦截器:在删除属性时触发依赖更新(这是 Vue 3 相比 Vue 2 的改进之一)
deleteProperty(target, key) {
// 检查属性是否真的存在于该对象上(不包括原型链上的属性)
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 执行删除操作
const result = Reflect.deleteProperty(target, key)
// 只有属性确实存在且删除成功时,才触发更新
if (hadKey && result) {
trigger(target, key)
}
return result
},
})
// 将创建好的 proxy 缓存起来,下次对同一对象调用 reactive 时直接返回
reactiveMap.set(target, proxy)
return proxy as T
}关键设计决策
1. 惰性递归(Lazy Reactive)
Vue 2 在初始化时递归遍历所有属性进行 defineProperty,性能开销大。
Vue 3 采用惰性转换 —— 只在 get 访问到深层对象时才递归调用 reactive:
ts
// 只有真正访问到 obj.nested 时,nested 才会被转换为响应式
if (isObject(res)) {
return reactive(res)
}2. Reflect 而不是直接操作
使用 Reflect.get / Reflect.set 而不是 target[key],是因为需要正确传递 receiver(即 proxy 本身),确保 getter/setter 中的 this 指向 proxy 而非原始对象。
3. 同一对象只创建一个 proxy
通过 reactiveMap 缓存,避免对同一个对象重复创建 proxy:
ts
const reactiveMap = new WeakMap<object, any>()
// 如果已经存在 proxy,直接返回
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
return existingProxy
}实现 track(依赖收集)
track 在 get 拦截器中调用,将当前正在执行的 effect 添加到对应属性的依赖集合中:
ts
// 当前正在执行的 effect 实例,track 时通过它来建立依赖关系
let activeEffect: ReactiveEffect | undefined
export function track(target: object, key: unknown) {
// 如果没有正在执行的 effect,说明当前不在响应式上下文中,无需收集依赖
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()))
}
// 避免重复收集:只有当前 effect 还没被收集到该属性的依赖中时才添加
if (!dep.has(activeEffect)) {
dep.add(activeEffect) // 将当前 effect 添加到属性的依赖集合中(属性 → effect 的映射)
activeEffect.deps.push(dep) // 让 effect 也反向记录自己被哪些 dep 收集了,便于后续清理无效依赖
}
}activeEffect.deps.push(dep) 这行很关键 —— 让 effect 也记住自己被哪些 dep 收集了,后续清理依赖时会用到。
实现 trigger(触发更新)
trigger 在 set 拦截器中调用,找到对应属性的所有依赖 effect 并执行:
ts
export function trigger(target: object, key: unknown) {
// 获取该对象的依赖映射表
const depsMap = targetMap.get(target)
if (!depsMap) return // 该对象从未被 track 过,无需触发
// 获取该属性对应的依赖集合
const dep = depsMap.get(key)
if (!dep) return // 该属性没有任何依赖,无需触发
// 复制一份依赖集合,避免在遍历过程中因 effect 执行导致原集合变化引发无限循环
const effects = new Set<ReactiveEffect>()
dep.forEach((effect) => {
// 避免 effect 在自己的执行过程中重复触发(防止无限递归)
if (effect !== activeEffect) {
effects.add(effect)
}
})
// 遍历执行所有依赖的 effect
effects.forEach((effect) => {
if (effect.scheduler) {
// 如果 effect 配置了调度器,优先使用调度器(computed/watch/组件更新等都依赖此机制实现自定义调度策略)
effect.scheduler()
} else {
// 没有调度器则直接执行 effect 的 run 方法
effect.run()
}
})
}为什么要复制一份 effects?
直接遍历 dep 执行 effect 时,effect 执行可能导致 dep 集合发生变化(添加或删除),导致无限循环。复制一份可以避免这个问题。
scheduler 的作用
如果 effect 有 scheduler,就调用 scheduler 而不是直接执行 run。这是后续 computed、watch、组件更新等高级功能的基础 —— 它们都需要自定义的调度策略。
工具函数
ts
// 使用 const enum 定义响应式标识符,编译时会被内联替换为字符串常量,零运行时开销
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive', // 标识一个对象是否为 reactive 代理
RAW = '__v_raw', // 用于获取 reactive 代理背后的原始对象
}
// 判断一个值是否为响应式对象:通过访问特殊标识属性触发 proxy 的 get 拦截器
export function isReactive(value: unknown): boolean {
return !!(value && (value as any)[ReactiveFlags.IS_REACTIVE])
}
// 获取响应式对象的原始值:通过访问 __v_raw 触发 get 拦截器返回原始对象
// 递归调用 toRaw 是为了处理多层代理嵌套的情况
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as any)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}测试用例
ts
describe('reactive', () => {
// 测试 reactive 是否正确创建了响应式代理对象
it('should return reactive object', () => {
const original = { foo: 1 }
const observed = reactive(original)
// 代理对象应该是一个新对象,而非原始对象本身
expect(observed).not.toBe(original)
// 代理对象应该能被 isReactive 正确识别为响应式对象
expect(isReactive(observed)).toBe(true)
// 原始对象不应该被误判为响应式对象
expect(isReactive(original)).toBe(false)
// 代理对象应该能正确读取到原始对象的属性值
expect(observed.foo).toBe(1)
})
// 测试嵌套对象是否也被惰性地转换为响应式
it('should make nested values reactive', () => {
const original = { nested: { foo: 1 } }
const observed = reactive(original)
// 访问嵌套属性时,返回值也应该是响应式的(惰性转换的体现)
expect(isReactive(observed.nested)).toBe(true)
})
// 测试同一对象多次调用 reactive 是否返回同一个代理(缓存机制)
it('should return same proxy for same object', () => {
const original = { foo: 1 }
// 两次调用应该返回完全相同的 proxy 实例
expect(reactive(original)).toBe(reactive(original))
})
})本节小结
- 三层 Map 结构 —
targetMap → depsMap → dep,精确追踪每个属性的依赖 - Proxy 拦截 — get 时 track 收集依赖,set 时 trigger 触发更新
- 惰性递归 — 只在访问时才递归转换,优于 Vue 2 的初始化遍历
- Reflect API — 正确处理 receiver,确保 this 指向
下一节我们实现 effect,让整个响应式链路跑通。