Appearance
第 4 章 @vue/reactivity 源码深度剖析(上):reactive / ref / track / trigger / computed
本章要点
- reactive() 的完整实现:Proxy handler 的五大拦截陷阱
- ref() 的设计取舍:为什么基本类型需要 .value 包装
- track() 与 trigger():依赖收集与触发更新的核心机制
- computed() 的惰性求值:如何用版本号实现"不读不算"
- 从 WeakMap 到 Link 链表:依赖存储结构的演进
深夜的代码审查室里,一位资深工程师正在排查一个诡异的 Bug:用户修改了购物车中某件商品的数量,价格却没有联动更新。他打开 Vue DevTools,发现 cartTotal 这个 computed 属性的依赖列表中居然没有 quantity——但模板里明明写着 {{ item.price * item.quantity }}。
"依赖是什么时候被收集的?"他自言自语,然后打开了 packages/reactivity/src/reactive.ts。
两个小时后,他不仅修复了 Bug(一个在条件分支中遗漏的响应式解包),还彻底理解了 Vue 响应式系统的依赖追踪机制。他后来告诉我:"那两个小时比我看十篇博文都值。因为源码回答的不是'这个 API 怎么用',而是'这个系统怎么想'。"
本章,我们就来做同样的事——打开 @vue/reactivity 的源码,逐行解析 reactive()、ref()、track()、trigger() 和 computed() 的完整实现。
4.1 reactive():Proxy 的五大拦截陷阱
从使用到实现
reactive() 是 Vue 3 中最基础的响应式 API。它接收一个普通对象,返回一个响应式代理:
typescript
import { reactive } from 'vue'
const state = reactive({
user: { name: 'Alice', age: 25 },
items: [1, 2, 3]
})
state.user.name = 'Bob' // 触发更新
state.items.push(4) // 触发更新
delete state.user.age // 触发更新
'name' in state.user // 被追踪表面看,reactive() 只是用 Proxy 包了一层。但当你打开源码时,会发现 Proxy handler 中的每一个拦截器(trap)都充满了精心设计的细节。
reactive() 的入口
typescript
// packages/reactivity/src/reactive.ts
export function reactive<T extends object>(target: T): Reactive<T> {
// 如果已经是 readonly,直接返回
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false, // isReadonly
mutableHandlers, // 对象的 Proxy handler
mutableCollectionHandlers, // Map/Set 的 Proxy handler
reactiveMap // WeakMap 缓存
)
}注意两个关键细节:
两套 handler:普通对象和集合类型(Map、Set、WeakMap、WeakSet)使用不同的 Proxy handler,因为集合类型的操作方式(
.get()、.set()、.add())与普通对象(.prop、obj[key])完全不同。WeakMap 缓存:同一个对象只会被代理一次。重复调用
reactive(obj)返回同一个代理实例。
typescript
// packages/reactivity/src/reactive.ts
function createReactiveObject(
target: object,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
// 1. 非对象类型直接返回
if (!isObject(target)) {
return target
}
// 2. 已经是代理了,直接返回(除非要对 reactive 对象做 readonly)
if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target
}
// 3. 检查缓存
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 4. 检查目标类型是否可以被代理
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target // ← 标记了 __v_skip 或被冻结的对象不代理
}
// 5. 创建代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION
? collectionHandlers // Map/Set
: baseHandlers // 普通对象/数组
)
// 6. 存入缓存
proxyMap.set(target, proxy)
return proxy
}🔥 深度洞察
getTargetType()函数会检查对象的Object.isExtensible()状态。被冻结(Object.freeze())或被密封(Object.seal())的对象不会被代理。这不是一个任意的限制——Proxy规范要求代理的行为必须与目标对象的不变量(invariant)一致。如果目标对象的属性是不可配置的,Proxy 的gettrap 必须返回与目标属性相同的值。对冻结对象创建响应式代理会导致 Proxy 内部抛出 TypeError——Vue 选择在入口处就避免这种情况。
mutableHandlers:五大拦截陷阱
mutableHandlers 是普通对象的 Proxy handler,包含五个陷阱函数:
typescript
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get, // 拦截属性读取 → 依赖收集
set, // 拦截属性赋值 → 触发更新
deleteProperty, // 拦截 delete → 触发更新
has, // 拦截 in 操作符 → 依赖收集
ownKeys // 拦截 Object.keys() / for...in → 依赖收集
}Trap 1: get — 属性读取与依赖收集
typescript
// packages/reactivity/src/baseHandlers.ts(简化)
function get(target: object, key: string | symbol, receiver: object) {
// 1. 内部标志位处理
if (key === ReactiveFlags.IS_REACTIVE) return true
if (key === ReactiveFlags.IS_READONLY) return false
if (key === ReactiveFlags.RAW) {
if (receiver === reactiveMap.get(target)) {
return target // ← toRaw() 的实现基础
}
}
const targetIsArray = isArray(target)
// 2. 数组方法的特殊处理
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 3. 正常的属性读取
const res = Reflect.get(target, key, receiver)
// 4. Symbol 和不可追踪的 key 不收集依赖
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 5. 依赖收集 ← 核心!
track(target, TrackOpTypes.GET, key)
// 6. 如果值是 ref,自动解包
if (isRef(res)) {
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 7. 如果值是对象,递归代理(惰性代理)
if (isObject(res)) {
return reactive(res) // ← 懒代理:只有被访问的属性才会被代理
}
return res
}这个 get trap 中有几个精妙的设计值得深入讨论:
惰性代理(Lazy Proxy)
当你执行 reactive({ a: { b: { c: 1 } } }) 时,Vue 并不会递归地对所有嵌套对象创建 Proxy。只有当 state.a 被访问时,{ b: { c: 1 } } 才会被代理;只有当 state.a.b 被访问时,{ c: 1 } 才会被代理。
这是一个关键的性能优化——如果一个对象有 100 个嵌套属性,但用户只使用了其中 3 个,其余 97 个属性的 Proxy 创建开销就被完全避免了。
自动 ref 解包
如果 reactive 对象的某个属性是一个 ref,读取时会自动解包:
typescript
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 0(而不是 ref 对象)
// 等价于 state.count.value,但不需要写 .value但注意第 6 步的条件判断:数组中的 ref 元素不会自动解包。这是因为数组索引操作(arr[0])在语义上不应该"穿透"包装对象——你期望 arr[0] 返回数组中实际存储的元素,而不是被悄悄解包后的值。
Trap 2: set — 属性赋值与触发更新
typescript
// packages/reactivity/src/baseHandlers.ts(简化)
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
// 1. 如果旧值是 ref 而新值不是,更新 ref 的 .value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 2. 判断是新增还是修改
const hadKey = isArray(target)
? Number(key) < target.length
: hasOwn(target, key)
// 3. 执行真正的赋值
const result = Reflect.set(target, key, value, receiver)
// 4. 只有代理自身(非原型链)的操作才触发更新
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
}关键细节:
- 区分 ADD 和 SET:新增属性和修改属性触发不同类型的更新。这对于
watch的深度监听和数组的length响应非常重要。 hasChanged检查:如果新旧值相同(使用Object.is比较),不触发更新。这避免了无意义的重渲染。- 原型链保护:只有对代理自身的操作才触发更新,继承链上的操作被忽略。
Trap 3–5: deleteProperty / has / ownKeys
typescript
// deleteProperty — delete obj.key
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key) // ← 删除也能触发更新
}
return result
}
// has — 'key' in obj
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key) // ← in 操作符也能追踪
}
return result
}
// ownKeys — Object.keys(obj) / for...in
function ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY // ← 追踪迭代操作
)
return Reflect.ownKeys(target)
}💡 最佳实践
ownKeystrap 追踪的是ITERATE_KEY,而非具体的属性名。这意味着当你使用Object.keys(state)或for...in遍历对象时,新增或删除属性都会触发依赖更新。这就是为什么v-for遍历响应式对象时,新增属性能够自动触发重渲染——不需要像 Vue 2 那样使用Vue.set()。
数组方法的特殊处理
数组的响应式处理比普通对象复杂得多。Vue 对几类数组方法做了特殊拦截:
typescript
// packages/reactivity/src/baseHandlers.ts(简化)
const arrayInstrumentations: Record<string, Function> = {}
// 查找方法:includes、indexOf、lastIndexOf
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this)
// 追踪数组每个元素的访问
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// 先用原始参数查找
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果没找到,用 toRaw 后的参数再试一次
return arr[key](...args.map(toRaw))
}
return res
}
})
// 变异方法:push、pop、shift、unshift、splice
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking() // ← 暂停依赖收集!
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking() // ← 恢复依赖收集
return res
}
})