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
}