Skip to content

第 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 缓存
  )
}

注意两个关键细节:

  1. 两套 handler:普通对象和集合类型(Map、Set、WeakMap、WeakSet)使用不同的 Proxy handler,因为集合类型的操作方式(.get().set().add())与普通对象(.propobj[key])完全不同。

  2. 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 的 get trap 必须返回与目标属性相同的值。对冻结对象创建响应式代理会导致 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
}

基于 VitePress 构建