Vue 响应式原理完全指南:从 defineProperty 到 Alien Signals

基于 vuejs/core reactivity 包全部 11 个源码文件,彻底拆解 Vue 响应式系统每一层实现——ReactiveFlags、懒代理、数组 hack、collectionHandlers、RefImpl、双向链表、版本计数、computed 双重身份、watch/watchEffect 万物皆 effect、effectScope 生命周期管理

核心结论先放这里: refreactivecomputedwatchwatchEffect、组件渲染——底层全是围绕 ReactiveEffect 展开的。区别只有三个维度:执行时机(lazy/eager)、是否缓存结果、scheduler 策略

源码版本:Vue 3.5.39 stable · 3.6.0-beta.17 pre-release
源码文件:effect.ts · ref.ts · reactive.ts · baseHandlers.ts · collectionHandlers.ts · arrayInstrumentations.ts · computed.ts · dep.ts · effectScope.ts · watch.ts · index.ts(共 11 个文件,3863 行)


一、Vue 2:defineProperty 的三个死穴

function defineReactive(obj, key, val) {
  const dep = new Dep()  // 每个属性一个 Dep(内部是 Set<Watcher>)
  Object.defineProperty(obj, key, {
    get() { dep.depend(); return val },
    set(v) { if (v !== val) { val = v; dep.notify() } }
  })
}

死穴一:新增属性不响应。 defineProperty 在对象创建时遍历所有 key,之后新增的 key 没有 getter/setter。需要 Vue.set hack。

死穴二:数组索引/length 不响应。 arr[0] = xarr.length = 0 都不触发。只能 monkey-patch push/pop/splice 等 7 个方法,覆盖不完整。

死穴三:初始化递归开销。 observe(data) 在组件创建时递归遍历整个 data 对象,data 越深越慢。

Vue 2 的 Watcher 是 Vue 3 ReactiveEffect 的前身,computedwatch、组件渲染都是不同配置的 Watcher。这条主线在 Vue 3 里延续。


二、ReactiveFlags:所有工具函数的基础

Vue 3 在对象上打标记位,所有工具函数都依赖这套标记:

// constants.ts
export enum ReactiveFlags {
  SKIP        = '__v_skip',        // markRaw() 打的标记,createReactiveObject 里直接返回
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW  = '__v_isShallow',
  IS_REF      = '__v_isRef',
  RAW         = '__v_raw',         // toRaw() 通过它剥代理层
}

工具函数实现全是读标记位,O(1):

isRef(r)      → r?.[ReactiveFlags.IS_REF] === true
isReactive(v) → !!(v?.[ReactiveFlags.IS_REACTIVE])
isReadonly(v) → !!(v?.[ReactiveFlags.IS_READONLY])
isShallow(v)  → !!(v?.[ReactiveFlags.IS_SHALLOW])
isProxy(v)    → !!(v?.[ReactiveFlags.RAW])           // reactive 和 readonly 都算
toRaw(v)      → 递归读 v[ReactiveFlags.RAW] 直到没有为止

// markRaw:只在 value 可扩展时才打标记(避免 Object.freeze 的对象报错)
markRaw(v)    → if (Object.isExtensible(v)) def(v, ReactiveFlags.SKIP, true)

isReactive 有一个细节:readonly(reactive(obj)) 的结果既是 readonly 也是 reactive,需要先剥 readonly 层再判断:

export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])  // 剥一层再判断
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

三、reactive:四张 WeakMap + 懒代理

createReactiveObject:所有 reactive 系 API 的核心

// reactive.ts
export const reactiveMap        = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap        = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  if (!isObject(target)) return target              // 原始值直接返回

  // 已经是 Proxy 了,直接返回(除非是 reactive 上套 readonly)
  if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) return target

  // markRaw 打了 SKIP 或对象不可扩展,直接返回
  if (target[ReactiveFlags.SKIP] || !Object.isExtensible(target)) return target

  // 同一 target 不重复创建 Proxy,直接返回缓存
  const existingProxy = proxyMap.get(target)
  if (existingProxy) return existingProxy

  // Map/Set/WeakMap/WeakSet → collectionHandlers;Object/Array → baseHandlers
  const targetType = targetTypeMap(toRawType(target))
  if (targetType === TargetType.INVALID) return target

  const proxy = new Proxy(target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

四张 WeakMap 确保同一 target 对应同一 Proxy 实例,不会重复创建。

懒代理:访问时才递归,不是初始化时

baseHandlers.tsget 拦截里,读到嵌套对象时才按需创建 Proxy:

// baseHandlers.ts(BaseReactiveHandler.get)
get(target, key, receiver) {
  // 处理 ReactiveFlags 的读取
  if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
  if (key === ReactiveFlags.IS_READONLY) return isReadonly
  if (key === ReactiveFlags.IS_SHALLOW)  return isShallow
  if (key === ReactiveFlags.RAW) {
    // 验证 receiver 是否是对应 proxyMap 里的代理,防止用户自定义 Proxy 套娃
    if (receiver === (isReadonly ? isShallow ? shallowReadonlyMap : readonlyMap
                                 : isShallow ? shallowReactiveMap : reactiveMap).get(target)
        || Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) {
      return target
    }
    return  // 不是对应的代理,返回 undefined
  }

  const targetIsArray = isArray(target)
  if (!isReadonly) {
    // 数组方法拦截(从 arrayInstrumentations 取 patch 版本)
    if (targetIsArray && arrayInstrumentations[key]) return arrayInstrumentations[key]
    if (key === 'hasOwnProperty') return hasOwnProperty  // patch hasOwnProperty,track HAS
  }

  const res = Reflect.get(target, key, isRef(target) ? target : receiver)

  // Symbol 内置属性和 __proto__ 等不追踪
  if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) return res

  if (!isReadonly) track(target, TrackOpTypes.GET, key)
  if (isShallow) return res   // shallowReactive 到此为止,不递归

  // reactive 内部 ref 自动解包(数组整数索引不解包)
  if (isRef(res)) {
    return targetIsArray && isIntegerKey(key) ? res : res.value
  }

  // 懒代理:访问时才创建,不是初始化时递归
  if (isObject(res)) {
    return isReadonly ? readonly(res) : reactive(res)
  }

  return res
}

Vue 2 是 observe(data) 初始化时递归遍历所有嵌套;Vue 3 是 obj.a.b.c 访问到 a 时才对 a 的值创建 Proxy,初始化成本 O(1)

set handler:隐式 ref 赋值

MutableReactiveHandler.set 里有一个隐式行为:如果旧值是 ref 而新值不是,直接写 oldValue.value

set(target, key, value, receiver) {
  let oldValue = target[key]
  if (!this._isShallow) {
    const isOldValueReadonly = isReadonly(oldValue)
    if (!isShallow(value) && !isReadonly(value)) {
      oldValue = toRaw(oldValue)
      value = toRaw(value)
    }
    // reactive 内部 ref 的隐式赋值
    if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
      if (isOldValueReadonly) return true  // readonly ref,静默失败
      else { oldValue.value = value; return true }
    }
  }
  const hadKey = isArray(target) && isIntegerKey(key)
    ? Number(key) < target.length
    : hasOwn(target, key)
  const result = Reflect.set(target, key, value, isRef(target) ? target : receiver)
  if (target === toRaw(receiver)) {  // 防止原型链上的 set 被误触发
    if (!hadKey) trigger(target, TriggerOpTypes.ADD, key, value)
    else if (hasChanged(value, oldValue)) trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return result
}

ownKeys / has / deleteProperty

// ownKeys:for...in 和 Object.keys() 触发,数组用 'length',对象用 ITERATE_KEY
ownKeys(target) {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

// has:in 操作符触发,track HAS(不追踪内置 Symbol)
has(target, key) {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) track(target, TrackOpTypes.HAS, key)
  return result
}

// deleteProperty:trigger DELETE
deleteProperty(target, key) {
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  return result
}

四、arrayInstrumentations:数组的两类特殊处理

Vue 3.5 把数组 patch 逻辑抽成了独立文件 arrayInstrumentations.ts(372行),涵盖几乎所有数组方法

问题一:查找方法查不到原始对象

const obj = {}
const arr = reactive([obj])
arr.includes(obj)  // ❌ false!arr[0] 返回的是 obj 的代理,=== 不相等

源码用 searchProxy 函数处理 includes/indexOf/lastIndexOf

function searchProxy(self, method, args) {
  const arr = toRaw(self)
  track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
  // 先用原始参数找(参数可能是代理)
  const res = arr[method](...args)
  // 找不到时,把参数 toRaw 再找一遍
  if ((res === -1 || res === false) && isProxy(args[0])) {
    args[0] = toRaw(args[0])
    return arr[method](...args)
  }
  return res
}

问题二:push/pop 读写 length 导致无限循环

const arr = reactive([])
watchEffect(() => arr.push(1))
// push 内部读 length(track)→ 写 length(trigger)→ effect 重跑 → 死循环

源码用 noTracking 函数处理 push/pop/shift/unshift/splice

function noTracking(self, method, args = []) {
  pauseTracking()   // shouldTrack = false,压入 trackStack
  startBatch()
  const res = toRaw(self)[method].apply(self, args)
  endBatch()
  resetTracking()   // 弹出 trackStack,恢复上一个状态
  return res
}

pauseTracking/resetTracking 操作全局 trackStack: boolean[],支持嵌套。

其余方法的处理策略

  • 迭代类Symbol.iterator/entries/values/keys/forEach/map/filter/find 等):用 shallowReadArray track ARRAY_ITERATE_KEY,回调里的 item 用 toWrapped 包成响应式
  • concat:调 reactiveReadArray(track ARRAY_ITERATE_KEY + 返回 raw 数组的响应式映射)
  • reduce/reduceRight:单独的 reduce 函数,处理初始值也需要 wrap 的 edge case
  • toReversed/toSorted/toSpliced:ES2023 新方法,调 reactiveReadArray 后再调原生方法

三个核心 iterate key:

ITERATE_KEY         = Symbol('Object iterate')   // 对象/Map 增删、for...in
MAP_KEY_ITERATE_KEY = Symbol('Map keys iterate') // Map.keys(),只有 key 变化时触发
ARRAY_ITERATE_KEY   = Symbol('Array iterate')    // 数组迭代方法

五、collectionHandlers:Map/Set 的代理策略

Map/Set/WeakMap/WeakSet 无法用普通 set handler 拦截方法调用(map.set(k,v) 触发的是 get 拦截到 'set' 字符串,再调用该方法)。

源码用 createInstrumentations(readonly, shallow) 工厂生成四种组合的 instrumentations 对象,collectionHandlers 只有一个 get trap,读到方法名时返回 patch 版本:

function createInstrumentationGetter(isReadonly, shallow) {
  const instrumentations = createInstrumentations(isReadonly, shallow)
  return (target, key, receiver) => {
    if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
    if (key === ReactiveFlags.IS_READONLY) return isReadonly
    if (key === ReactiveFlags.RAW) return target
    // 如果 key 在 instrumentations 里且 target 上也有这个方法,返回 patch 版本
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target ? instrumentations : target,
      key, receiver
    )
  }
}

export const mutableCollectionHandlers   = { get: createInstrumentationGetter(false, false) }
export const shallowCollectionHandlers   = { get: createInstrumentationGetter(false, true) }
export const readonlyCollectionHandlers  = { get: createInstrumentationGetter(true, false) }
export const shallowReadonlyCollectionHandlers = { get: createInstrumentationGetter(true, true) }

核心 patch 方法的追踪策略:

// Map.get:同时 track 代理 key 和原始 key(参数可能是代理)
get(key) {
  const rawKey = toRaw(key)
  if (!readonly) {
    if (hasChanged(key, rawKey)) track(rawTarget, TrackOpTypes.GET, key)
    track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  // 先找 key,再找 rawKey
  if (has.call(rawTarget, key)) return wrap(target.get(key))
  if (has.call(rawTarget, rawKey)) return wrap(target.get(rawKey))
}

// size:track ITERATE_KEY(任何增删都会触发)
get size() {
  track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return target.size
}

// has:同时 track 代理 key 和原始 key
has(key) {
  const rawKey = toRaw(key)
  if (!readonly) {
    if (hasChanged(key, rawKey)) track(rawTarget, TrackOpTypes.HAS, key)
    track(rawTarget, TrackOpTypes.HAS, rawKey)
  }
  return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey)
}

// Map.set:区分 ADD(新 key)和 SET(已有 key)
set(key, value) {
  if (!shallow && !isShallow(value) && !isReadonly(value)) value = toRaw(value)
  const target = toRaw(this)
  let hadKey = target.has(key)
  if (!hadKey) { key = toRaw(key); hadKey = target.has(key) }
  const oldValue = target.get(key)
  target.set(key, value)
  if (!hadKey) trigger(target, TriggerOpTypes.ADD, key, value)
  else if (hasChanged(value, oldValue)) trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  return this
}

// Set.add:检查三种可能的 key(代理/rawKey/rawValue)
add(value) {
  const rawValue = toRaw(value)
  const valueToAdd = !shallow && !isShallow(value) && !isReadonly(value) ? rawValue : value
  const hadKey = proto.has.call(target, valueToAdd)
    || (hasChanged(value, valueToAdd) && proto.has.call(target, value))
    || (hasChanged(rawValue, valueToAdd) && proto.has.call(target, rawValue))
  if (!hadKey) {
    target.add(valueToAdd)
    trigger(target, TriggerOpTypes.ADD, valueToAdd, valueToAdd)
  }
  return this
}

// clear:trigger CLEAR,通知所有依赖
clear() {
  const hadItems = target.size !== 0
  const result = target.clear()
  if (hadItems) trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  return result
}

// keys/values/entries/Symbol.iterator/forEach:
// Map.keys() track MAP_KEY_ITERATE_KEY(只有 key 变化触发,value 变不触发)
// 其他迭代 track ITERATE_KEY

trigger 函数根据 TriggerOpTypes 决定触发哪些 dep:

// dep.ts trigger 函数的分支逻辑
switch (type) {
  case TriggerOpTypes.ADD:
    if (!isArray) run(depsMap.get(ITERATE_KEY))
    if (isMap) run(depsMap.get(MAP_KEY_ITERATE_KEY))  // Map 新增 key 也触发 keys()
    if (isArray && isIntegerKey(key)) run(depsMap.get('length'))
    break
  case TriggerOpTypes.DELETE:
    if (!isArray) run(depsMap.get(ITERATE_KEY))
    if (isMap) run(depsMap.get(MAP_KEY_ITERATE_KEY))
    break
  case TriggerOpTypes.SET:
    if (isMap) run(depsMap.get(ITERATE_KEY))  // Map.set 触发 forEach/values/entries
    break
  case TriggerOpTypes.CLEAR:
    depsMap.forEach(run)  // 清空,触发所有 dep
    break
}
// 数组 length 变小:触发所有 key >= newLength 的 dep 和 ARRAY_ITERATE_KEY
if (key === 'length' && isArray) {
  const newLength = Number(newValue)
  depsMap.forEach((dep, key) => {
    if (key === 'length' || key === ARRAY_ITERATE_KEY || (!isSymbol(key) && key >= newLength))
      run(dep)
  })
}

六、dep.ts:依赖追踪的核心数据结构

globalVersion 全局计数器

export let globalVersion = 0
// 任何 dep.trigger() 都会让它 ++
// computed 用它做快速路径:如果 computed.globalVersion === globalVersion,不用重算

每条依赖边(“subscriber X 依赖了 dep Y”)是一个 Link 节点,同时参与两个双向链表:

export class Link {
  version: number          // 上次收集时 dep 的 version,用于 cleanupDeps 和 isDirty

  nextDep?: Link           // sub 视角:我依赖的下一个 dep
  prevDep?: Link
  nextSub?: Link           // dep 视角:订阅我的下一个 sub
  prevSub?: Link
  prevActiveLink?: Link    // prepareDeps/cleanupDeps 用的临时指针

  constructor(public sub: Subscriber, public dep: Dep) {
    this.version = dep.version
    this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = undefined
  }
}
dep 视角(谁订阅了我):  Dep.subs → L1 ↔ L2 ↔ L3
sub 视角(我依赖了谁):  Effect.deps → LA ↔ LB ↔ LC
两个方向共用同一批 Link 节点,零重复存储。

Dep 类

export class Dep {
  version = 0
  activeLink?: Link = undefined    // 当前活跃 sub 对应的 link(prepareDeps/track 用)
  subs?: Link = undefined          // 订阅者链表(尾节点)
  subsHead?: Link                  // 订阅者链表头(DEV only,onTrigger 顺序用)
  map?: KeyToDepMap = undefined    // 所属的 depsMap,用于 dep 无订阅者时自动从 targetMap 删除
  key?: unknown = undefined        // 对应的 key
  sc: number = 0                   // subscriber count,为 0 时可以 GC
  readonly __v_skip = true         // 防止 Dep 实例被 reactive 代理

  constructor(public computed?: ComputedRefImpl) {}
}

Dep.track() 的链表节点复用逻辑:

track(debugInfo?): Link | undefined {
  if (!activeSub || !shouldTrack || activeSub === this.computed) return

  let link = this.activeLink
  if (link === undefined || link.sub !== activeSub) {
    // 新建 link,追加到 sub 的依赖链表尾部
    link = this.activeLink = new Link(activeSub, this)
    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link
    } else {
      link.prevDep = activeSub.depsTail
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link
    }
    addSub(link)
  } else if (link.version === -1) {
    // 复用上次的 link(同一 sub 再次访问同一 dep)
    link.version = this.version
    // 如果这个 link 不在链表尾,移到尾部(保持访问顺序)
    if (link.nextDep) {
      const next = link.nextDep
      next.prevDep = link.prevDep
      if (link.prevDep) link.prevDep.nextDep = next
      link.prevDep = activeSub.depsTail
      link.nextDep = undefined
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link
      if (activeSub.deps === link) activeSub.deps = next
    }
  }
  return link
}

addSub 的 computed 懒激活逻辑:

function addSub(link: Link) {
  link.dep.sc++
  if (link.sub.flags & EffectFlags.TRACKING) {
    const computed = link.dep.computed
    // computed 第一次被订阅:启用 TRACKING,递归订阅它自身的所有 deps
    if (computed && !link.dep.subs) {
      computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
      for (let l = computed.deps; l; l = l.nextDep) addSub(l)
    }
    // 追加到 dep 的订阅者链表(尾部)
    const currentTail = link.dep.subs
    if (currentTail !== link) {
      link.prevSub = currentTail
      if (currentTail) currentTail.nextSub = link
    }
    link.dep.subs = link
  }
}

removeSub 的 soft 参数:

function removeSub(link: Link, soft = false) {
  // 从 dep 的订阅者链表里摘除这个 link
  if (dep.subs === link) {
    dep.subs = prevSub
    if (!prevSub && dep.computed) {
      // computed 没有订阅者了:解除 TRACKING,soft unsubscribe 自身的 deps
      dep.computed.flags &= ~EffectFlags.TRACKING
      for (let l = dep.computed.deps; l; l = l.nextDep) removeSub(l, true)
      // soft = true:不减 sc(computed 还持有引用,只是暂时不追踪)
    }
  }
  if (!soft && !--dep.sc && dep.map) {
    // sc 减到 0:从 targetMap 里删除这个 dep,允许 GC
    dep.map.delete(dep.key)
  }
}

内存对比(1000 refs + 2000 computed + 1000 effects):

版本 数据结构 内存
Vue 3.4 WeakMap → Map → Set(两份存储) 1426KB
Vue 3.5 双向链表 Link(一份) 631KB(-56%)

七、effect.ts:ReactiveEffect 核心原语

EffectFlags 位掩码

export enum EffectFlags {
  ACTIVE        = 1 << 0,  // 还活着(stop 后 false)
  RUNNING       = 1 << 1,  // 正在执行 fn
  TRACKING      = 1 << 2,  // 正在收集依赖(shouldTrack 配合)
  NOTIFIED      = 1 << 3,  // 已加入 batch 队列,防重复
  DIRTY         = 1 << 4,  // 确定需要重算
  ALLOW_RECURSE = 1 << 5,  // 允许 effect 递归触发自己
  PAUSED        = 1 << 6,  // 已暂停(pause/resume API)
  EVALUATED     = 1 << 7,  // computed 专用:已计算过至少一次
}

ReactiveEffect.run()

run(): T {
  if (!(this.flags & EffectFlags.ACTIVE)) return this.fn()  // 已 stop,直接跑不收集

  this.flags |= EffectFlags.RUNNING
  cleanupEffect(this)    // 执行上次 onEffectCleanup 注册的 cleanup 函数
  prepareDeps(this)      // 把所有现有 link.version 标为 -1("待确认")
  const prevEffect = activeSub
  const prevShouldTrack = shouldTrack
  activeSub = this       // 设为当前活跃订阅者
  shouldTrack = true
  try {
    return this.fn()     // 执行,fn 内读到响应式数据时自动 track
  } finally {
    cleanupDeps(this)    // 清除 version 还是 -1 的 link(条件分支里失效的依赖)
    activeSub = prevEffect
    shouldTrack = prevShouldTrack
    this.flags &= ~EffectFlags.RUNNING
  }
}

prepareDeps / cleanupDeps——条件分支依赖的精确清除:

// effect 里有 if (show.value) { a.value } else { b.value }
// show 从 true 变 false,a 就不是依赖了

function prepareDeps(sub) {
  for (let link = sub.deps; link; link = link.nextDep) {
    link.version = -1           // 所有现有 link 标为"待确认"
    link.prevActiveLink = link.dep.activeLink
    link.dep.activeLink = link
  }
}

function cleanupDeps(sub) {
  let head, tail = sub.depsTail, link = tail
  while (link) {
    const prev = link.prevDep
    if (link.version === -1) {
      // 本次执行没有访问这个 dep → 移除
      if (link === tail) tail = prev
      removeSub(link)
      removeDep(link)
    } else {
      head = link
    }
    link.dep.activeLink = link.prevActiveLink
    link.prevActiveLink = undefined
    link = prev
  }
  sub.deps = head
  sub.depsTail = tail
}

notify / trigger / batch

notify(): void {
  // RUNNING 时不响应(防递归),除非 ALLOW_RECURSE
  if (this.flags & EffectFlags.RUNNING && !(this.flags & EffectFlags.ALLOW_RECURSE)) return
  if (!(this.flags & EffectFlags.NOTIFIED)) batch(this)
}

trigger(): void {
  if (this.flags & EffectFlags.PAUSED) pausedQueueEffects.add(this)
  else if (this.scheduler) this.scheduler()
  else this.runIfDirty()
}

// batch:把 effect 加入批处理链表,endBatch 时统一执行
// computed 用 batchedComputed 链表(先处理),普通 effect 用 batchedSub 链表
export function batch(sub: Subscriber, isComputed = false): void {
  sub.flags |= EffectFlags.NOTIFIED
  if (isComputed) { sub.next = batchedComputed; batchedComputed = sub; return }
  sub.next = batchedSub; batchedSub = sub
}

isDirty:版本计数 + computed 递归

function isDirty(sub: Subscriber): boolean {
  for (let link = sub.deps; link; link = link.nextDep) {
    if (link.dep.version !== link.version ||
        (link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version))) {
      return true
    }
  }
  // Pinia testing module 向后兼容 hack
  if ((sub as any)._dirty) return true
  return false
}

onEffectCleanup / cleanupEffect

// 注册:只能在 effect 执行期间调用(activeSub 是 ReactiveEffect 时)
export function onEffectCleanup(fn: () => void, failSilently = false): void {
  if (activeSub instanceof ReactiveEffect) {
    activeSub.cleanup = fn
  }
}

// 执行:每次 run() 前调用,执行时 activeSub = undefined(cleanup 内不收集依赖)
function cleanupEffect(e: ReactiveEffect) {
  const { cleanup } = e
  e.cleanup = undefined
  if (cleanup) {
    const prevSub = activeSub
    activeSub = undefined
    try { cleanup() } finally { activeSub = prevSub }
  }
}

RefImpl 内部的 Suspense 防孤儿 effect

constructor(public fn: () => T) {
  if (activeEffectScope) {
    if (activeEffectScope.active) {
      activeEffectScope.effects.push(this)
    } else {
      // scope 已经 stop 了(组件在 Suspense 里等待 async setup 期间被卸载)
      // 如果不处理,这个 effect 会成为孤儿:没有 scope 管它,却能永远响应 dep 变化
      this.flags &= ~EffectFlags.ACTIVE  // 直接标记为非活跃
    }
  }
}

八、ref.ts:RefImpl 完整实现

为什么 ref 需要 .value

Proxy 只能代理对象,原始值(number/string/boolean)没法被代理。ref 要支持任意值,需要把它包进一个对象,通过 .value 的 getter/setter 拦截读写:

class RefImpl<T = any> {
  _value: T
  private _rawValue: T    // toRaw 后的原始值,用于 hasChanged 比较
  dep: Dep = new Dep()
  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean

  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value)
    // 如果 value 是对象,用 reactive() 包,嵌套属性也响应式
    this._value = isShallow ? value : toReactive(value)
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }

  get value() {
    this.dep.track()    // 读 .value 时收集依赖
    return this._value
  }

  set value(newValue) {
    const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || isReadonly(newValue)
    newValue = useDirectValue ? newValue : toRaw(newValue)
    if (hasChanged(newValue, this._rawValue)) {   // Object.is 比较原始值
      this._rawValue = newValue
      this._value = useDirectValue ? newValue : toReactive(newValue)
      this.dep.trigger()
    }
  }
}

_rawValue 的作用: setter 里 hasChanged 比较的是 toRaw 后的值,不是 Proxy。避免 ref.value = sameObject 重复触发。

createRef 有去重逻辑: if (isRef(rawValue)) return rawValue——传入已是 ref 的值直接返回,不重复包装。

shallowRef / triggerRef / customRef

// shallowRef = createRef(value, true)
// isShallow=true 时 _value 直接存原始值,不 toReactive
// 只有替换整个 .value 才触发更新

// triggerRef:手动强制触发 shallowRef 的所有订阅者
export function triggerRef(ref: Ref): void {
  if ((ref as RefImpl).dep) {
    (ref as RefImpl).dep.trigger()
  }
}

// customRef:把 track/trigger 完全交给用户
class CustomRefImpl<T> {
  public dep: Dep
  constructor(factory: CustomRefFactory<T>) {
    const dep = (this.dep = new Dep())
    const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
    this._get = get; this._set = set
  }
  get value() { return (this._value = this._get()) }
  set value(v) { this._set(v) }
}

ref 解包的两种机制(原理不同)

reactive 内部解包(运行时,baseHandlers get 里):

if (isRef(res)) {
  // 数组整数索引不解包,其他情况自动 .value
  return targetIsArray && isIntegerKey(key) ? res : res.value
}

模板解包(运行时,proxyRefs):

setup() 返回的对象经过 proxyRefs 处理:

const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) =>
    key === ReactiveFlags.RAW ? target : unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value    // 模板里赋值自动走 .value =
      return true
    }
    return Reflect.set(target, key, value, receiver)
  }
}

export function proxyRefs<T extends object>(objectWithRefs: T): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs as ShallowUnwrapRef<T>  // 已经是 reactive,不需要再包
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
reactive 内部解包 模板解包(proxyRefs)
位置 baseHandlers get shallowUnwrapHandlers
深度 深层(嵌套对象里的 ref 也解包) 浅层(只解一层)
数组整数 key 不解包 不解包
赋值行为 set handler 里自动 oldValue.value = value 同上

toRef / ObjectRefImpl / GetterRefImpl

toRef 有三种重载,对应三种实现:

export function toRef(source, key?, defaultValue?) {
  if (isRef(source)) return source                            // 已是 ref,直接返回
  if (isFunction(source)) return new GetterRefImpl(source)   // 传函数 → 只读 getter ref
  if (isObject(source) && arguments.length > 1) return new ObjectRefImpl(source, key, defaultValue)
  return ref(source)                                         // 其他值 → 普通 ref
}

ObjectRefImpl——代理到原对象,不存值:

class ObjectRefImpl<T extends object, K extends keyof T> {
  readonly [ReactiveFlags.IS_REF] = true
  private _shallow: boolean
  private _raw: T

  constructor(private _object: T, key: K, private _defaultValue?: T[K]) {
    this._key = isSymbol(key) ? key : String(key) as K
    this._raw = toRaw(_object)
    // 判断是否需要解包:数组整数索引不解包,其他情况遍历 proxy 层看是否有解包
    let shallow = true, obj = _object
    if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) {
      do { shallow = !isProxy(obj) || isShallow(obj) } while (shallow && (obj = obj[ReactiveFlags.RAW]))
    }
    this._shallow = shallow
  }

  get value() {
    let val = this._object[this._key]
    if (this._shallow) val = unref(val)   // 浅层才解包
    return (this._value = val === undefined ? this._defaultValue! : val)
  }

  set value(newVal) {
    // 如果是浅层且原 key 是 ref,写到 ref 里
    if (this._shallow && isRef(this._raw[this._key])) {
      const nestedRef = this._object[this._key]
      if (isRef(nestedRef)) { nestedRef.value = newVal; return }
    }
    this._object[this._key] = newVal  // 否则直接写原对象
  }

  get dep() { return getDepFromReactive(this._raw, this._key) }  // dep 来自原对象
}

GetterRefImpl——只读,调 getter 取值:

class GetterRefImpl<T> {
  readonly [ReactiveFlags.IS_REF] = true
  readonly [ReactiveFlags.IS_READONLY] = true
  get value() { return (this._value = this._getter()) }
}
// 用法:const fooRef = toRef(() => props.foo)

九、computed.ts:双重身份的 effect

computed 同时是 Subscriber(订阅上游 ref/reactive)和 Dep(被下游 effect 订阅)。

export class ComputedRefImpl<T = any> implements Subscriber {
  _value: any = undefined
  readonly dep: Dep = new Dep(this)   // 作为 Dep,下游通过它订阅
  readonly __v_isRef = true
  readonly __v_isReadonly: boolean    // 没有 setter 时为 true

  deps?: Link; depsTail?: Link        // 作为 Subscriber,上游依赖链表
  flags: EffectFlags = EffectFlags.DIRTY  // 初始 DIRTY(lazy,第一次读时才算)
  globalVersion: number = globalVersion - 1  // 初始比全局低 1,保证第一次 refresh
  isSSR: boolean
  next?: Subscriber = undefined

  // 向后兼容(deprecated)
  effect: this = this

  constructor(public fn: ComputedGetter<T>, private readonly setter, isSSR) {
    this[ReactiveFlags.IS_READONLY] = !setter
    this.isSSR = isSSR
  }

  notify(): true | void {
    this.flags |= EffectFlags.DIRTY
    if (!(this.flags & EffectFlags.NOTIFIED) && activeSub !== this) {
      batch(this, true)    // isComputed=true,加入 batchedComputed 链表(先于普通 effect)
      return true          // 返回 true = "我是 computed,继续通知我的下游 dep"
    }
  }

  get value(): T {
    const link = this.dep.track()    // 把当前 activeSub 订阅到自己
    refreshComputed(this)            // 如果 DIRTY,重新计算
    if (link) link.version = this.dep.version  // 同步版本号
    return this._value
  }

  set value(newValue) {
    if (this.setter) this.setter(newValue)
  }
}

refreshComputed 的完整逻辑:

export function refreshComputed(computed: ComputedRefImpl): undefined {
  // TRACKING 且不 DIRTY:还在收集依赖阶段,不重算
  if (computed.flags & EffectFlags.TRACKING && !(computed.flags & EffectFlags.DIRTY)) return

  computed.flags &= ~EffectFlags.DIRTY

  // globalVersion 快速路径:全局没有任何响应式变化,直接用缓存
  if (computed.globalVersion === globalVersion) return
  computed.globalVersion = globalVersion

  // SSR 没有渲染 effect,computed 无订阅者,无法依赖 isDirty,每次都重算
  // 但如果 computed 没有 deps 且已经算过,也可以跳过(#12337)
  if (!computed.isSSR && computed.flags & EffectFlags.EVALUATED
      && ((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))) {
    return
  }

  computed.flags |= EffectFlags.RUNNING
  const dep = computed.dep
  activeSub = computed
  shouldTrack = true
  try {
    prepareDeps(computed)
    const value = computed.fn(computed._value)  // 传入 oldValue,支持 fn(prev) => next
    if (dep.version === 0 || hasChanged(value, computed._value)) {
      computed.flags |= EffectFlags.EVALUATED
      computed._value = value
      dep.version++   // 版本号 ++,下游 isDirty 时能感知到变化
    }
    // 如果值没变,dep.version 不变 → 下游 link.version === dep.version → 下游不重算
  } catch (err) {
    dep.version++     // 出错也要 version++,防止下游永远认为没变化
    throw err
  } finally {
    activeSub = prevSub
    shouldTrack = prevShouldTrack
    cleanupDeps(computed)
    computed.flags &= ~EffectFlags.RUNNING
  }
}

_dirty flag 完整流转:

初始:DIRTY(lazy,第一次读时才算)
↓ 读 .value → refreshComputed → 清除 DIRTY → 缓存 _value → dep.version 可能 ++
↓ 上游变化 → notify() → 设置 DIRTY → batch(this, true) → 通知下游
↓ 下游读 .value → refreshComputed
    → 值没变 → dep.version 不变 → 下游 isDirty 返回 false → 不传播
    → 值变了 → dep.version++ → 下游 isDirty 返回 true → 继续传播

十、watch.ts:万物皆 Effect

watch.ts 在 Vue 3.5 从 runtime-core/src/apiWatch.ts 迁移到 reactivity 包,实现了 baseWatch(现在就叫 watch),runtime-corewatch/watchEffect/watchPostEffect/watchSyncEffect 都在它之上通过 scheduleraugmentJob 参数扩展。

两个全局变量

const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
let activeWatcher: ReactiveEffect | undefined = undefined

// onEffectCleanup 把 cleanup 存到 effect.cleanup(单个)
// onWatcherCleanup 把 cleanup 存到 cleanupMap(多个,支持多次注册)
export function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) {
  if (owner) {
    let cleanups = cleanupMap.get(owner)
    if (!cleanups) cleanupMap.set(owner, (cleanups = []))
    cleanups.push(cleanupFn)   // 支持多个 cleanup
  }
}

watch 函数的完整逻辑

export function watch(source, cb?, options = EMPTY_OBJ): WatchHandle {
  const { immediate, deep, once, scheduler, augmentJob, call } = options

  // 1. 根据 source 类型构造 getter
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)          // shallowRef 强制触发
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source)     // 自动 deep traverse
    forceTrigger = true                       // reactive 对象强制触发
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () => source.map(s => ...)       // 多 source 映射
  } else if (isFunction(source)) {
    if (cb) {
      getter = source as () => any            // watch(getter, cb)
    } else {
      // watchEffect:getter 内自己管理 cleanup
      getter = () => {
        if (cleanup) {
          pauseTracking()   // cleanup 内不收集依赖
          try { cleanup() } finally { resetTracking() }
        }
        const currentEffect = activeWatcher
        activeWatcher = effect
        try {
          return call ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
                      : source(boundCleanup)
        } finally {
          activeWatcher = currentEffect
        }
      }
    }
  }

  // 2. deep 时包一层 traverse
  if (cb && deep) {
    const baseGetter = getter
    const depth = deep === true ? Infinity : deep
    getter = () => traverse(baseGetter(), depth)
  }

  // 3. once 选项:执行一次后自动 stop
  if (once && cb) {
    const _cb = cb
    cb = (...args) => { const res = _cb(...args); watchHandle(); return res }
  }

  // 4. INITIAL_WATCHER_VALUE:用对象引用而非 undefined
  // 因为 undefined 是合法的 oldValue(computed 返回 undefined)
  let oldValue: any = isMultiSource
    ? new Array(source.length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE

  // 5. job:实际执行逻辑
  const job = (immediateFirstRun?: boolean) => {
    if (!(effect.flags & EffectFlags.ACTIVE) || (!effect.dirty && !immediateFirstRun)) return

    if (cb) {
      const newValue = effect.run()
      if (immediateFirstRun || deep || forceTrigger ||
          (isMultiSource
            ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
            : hasChanged(newValue, oldValue))) {
        // 执行前先运行上次的 cleanup
        if (cleanup) cleanup()
        const currentWatcher = activeWatcher
        activeWatcher = effect
        try {
          const args = [
            newValue,
            // 第一次执行时 oldValue 传 undefined(不是 INITIAL_WATCHER_VALUE)
            oldValue === INITIAL_WATCHER_VALUE ? undefined
              : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? []
              : oldValue,
            boundCleanup
          ]
          oldValue = newValue
          call ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args) : cb!(...args)
        } finally {
          activeWatcher = currentWatcher
        }
      }
    } else {
      effect.run()  // watchEffect 直接跑
    }
  }

  // 6. 创建 effect,scheduler 来自 options(runtime-core 注入 queueJob)
  effect = new ReactiveEffect(getter)
  effect.scheduler = scheduler ? () => scheduler(job, false) : (job as EffectScheduler)

  // 7. cleanup 挂到 effect.onStop
  cleanup = effect.onStop = () => {
    const cleanups = cleanupMap.get(effect)
    if (cleanups) {
      call ? call(cleanups, WatchErrorCodes.WATCH_CLEANUP) : cleanups.forEach(fn => fn())
      cleanupMap.delete(effect)
    }
  }

  // 8. 初始执行
  if (cb) {
    if (immediate) job(true)        // immediate: true → 立刻执行 cb
    else oldValue = effect.run()    // lazy → 只跑 getter 收集依赖,记录初始值
  } else if (scheduler) {
    scheduler(job.bind(null, true), true)  // watchEffect with scheduler
  } else {
    effect.run()                    // watchEffect,立即执行
  }

  // 9. WatchHandle:可暂停/恢复/停止
  watchHandle.pause  = effect.pause.bind(effect)
  watchHandle.resume = effect.resume.bind(effect)
  watchHandle.stop   = watchHandle

  return watchHandle
}

reactiveGetter:watch reactive 对象时的深度策略

const reactiveGetter = (source: object) => {
  if (deep) return source           // deep: true/number → 由外层 traverse 处理
  if (isShallow(source) || deep === false || deep === 0)
    return traverse(source, 1)      // 只遍历根级属性
  return traverse(source)           // deep: undefined → 默认深度遍历
}

traverse:支持深度限制 + 循环引用

export function traverse(value, depth = Infinity, seen?: Map<unknown, number>): unknown {
  if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) return value

  seen = seen || new Map()
  if ((seen.get(value) || 0) >= depth) return value  // 已访问且深度足够,跳过
  seen.set(value, depth)
  depth--

  if (isRef(value)) {
    traverse(value.value, depth, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) traverse(value[i], depth, seen)
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => traverse(v, depth, seen))
  } else if (isPlainObject(value)) {
    for (const key in value) traverse(value[key], depth, seen)
    // 也遍历 Symbol 类型的 enumerable 属性
    for (const key of Object.getOwnPropertySymbols(value)) {
      if (Object.prototype.propertyIsEnumerable.call(value, key)) {
        traverse(value[key as any], depth, seen)
      }
    }
  }
  return value
}

注意 seen 用的是 Map<unknown, number> 而不是 Set,存的是 depth 值。这样同一个对象在不同深度被访问时,只要新的 depth 更深,就会继续遍历。

四种 effect 变体对比

ReactiveEffect(底层原语)
  ├── watchEffect  → eager + 无 cb + scheduler=queueJob(pre)
  ├── watch        → lazy + 有 cb + scheduler=queueJob + oldValue + forceTrigger
  ├── computed     → lazy + 无 scheduler + 有缓存 + 既是 sub 也是 dep
  └── 组件渲染     → eager + scheduler=queueJob(pre) + fn=renderComponentRoot
watchEffect watch computed 组件渲染
执行时机 eager lazy lazy(读时) eager
缓存
oldValue
也是 Dep
scheduler queueJob(pre) queueJob(post) 标记 DIRTY queueJob(pre)
cleanup onEffectCleanup / onWatcherCleanup onWatcherCleanup
pause/resume

十一、effectScope.ts:批量生命周期管理

每个 Vue 组件实例都有一个 EffectScope,组件卸载时调 scope.stop(),所有 watcher 和 computed 一次性全部停止。

export class EffectScope {
  private _active = true
  private _on = 0         // on()/off() 嵌套计数,支持多次 on/off 对称调用
  private _isPaused = false
  effects: ReactiveEffect[] = []
  cleanups: (() => void)[] = []
  parent: EffectScope | undefined
  scopes: EffectScope[] | undefined
  private index: number | undefined   // 在父 scope.scopes 数组里的下标,用于 O(1) 移除
  readonly __v_skip = true            // 防止 scope 被 reactive 代理

  constructor(public detached = false) {
    this.parent = activeEffectScope
    if (!detached && activeEffectScope) {
      // 非 detached:自动注册到父 scope
      this.index = (activeEffectScope.scopes ||= []).push(this) - 1
    }
  }

  run<T>(fn: () => T): T | undefined {
    if (this._active) {
      const currentEffectScope = activeEffectScope
      try {
        activeEffectScope = this   // fn 内创建的 effect 自动注册进来
        return fn()
      } finally {
        activeEffectScope = currentEffectScope
      }
    }
  }

  // on()/off():手动控制 activeEffectScope,用于非 run() 场景
  on(): void {
    if (++this._on === 1) { this.prevScope = activeEffectScope; activeEffectScope = this }
  }
  off(): void {
    if (this._on > 0 && --this._on === 0) { activeEffectScope = this.prevScope; this.prevScope = undefined }
  }

  pause(): void {
    if (this._active) {
      this._isPaused = true
      this.scopes?.forEach(s => s.pause())
      this.effects.forEach(e => e.pause())
    }
  }

  resume(): void {
    if (this._active && this._isPaused) {
      this._isPaused = false
      this.scopes?.forEach(s => s.resume())
      this.effects.forEach(e => e.resume())
    }
  }

  stop(fromParent?: boolean): void {
    if (this._active) {
      this._active = false
      this.effects.forEach(e => e.stop()); this.effects.length = 0
      this.cleanups.forEach(fn => fn()); this.cleanups.length = 0
      this.scopes?.forEach(s => s.stop(true)); this.scopes && (this.scopes.length = 0)

      // O(1) 从父 scope.scopes 里移除自己(swap with last)
      if (!this.detached && this.parent && !fromParent) {
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.parent = undefined   // 断开父引用,防止内存泄漏
    }
  }
}

export function onScopeDispose(fn: () => void, failSilently = false): void {
  if (activeEffectScope) activeEffectScope.cleanups.push(fn)
}

detached scope: 不注册到父 scope,父 stop 时不会自动 stop,需要手动管理。常用于在组件外创建的独立响应式逻辑。

onScopeDispose vs onUnmounted composable 里用 onScopeDispose 替代 onUnmounted,让 composable 在组件外独立使用时也能正确清理。


十二、调度器:批量更新与 flush 顺序

watch.ts 里的 watch 是 baseWatch,不带 scheduler。runtime-coreapiWatch.ts 通过 options.scheduler 注入调度逻辑:

// runtime-core/src/scheduler.ts(简化)
const queue: SchedulerJob[] = []
let isFlushing = false

export function queueJob(job: SchedulerJob) {
  if (!queue.includes(job)) {
    queue.push(job)
    queue.sort((a, b) => getId(a) - getId(b))  // 按 id 排序,父组件 id < 子组件 id
  }
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)           // 微任务批量刷新
  }
}

完整 flush 顺序:

同一轮事件循环里所有响应式变化入队
↓ 微任务触发 flushJobs()
↓ pre flush:watchEffect(flush:'pre') · watch(flush:'pre')
↓ 组件更新(按 id 升序,父先于子)
    beforeUpdate → renderComponentRoot → vdom diff → patch DOM
↓ post flush:watchEffect(flush:'post') · watch(flush:'post') · updated · 模板 ref 赋值

为什么用微任务而不是 MessageChannel? Vue 的更新精确追踪了变化的 key,每次更新量小,不需要中断,微任务够用。React 用 MessageChannel(宏任务)是因为 Fiber 时间切片需要可中断——微任务一旦开始无法被打断。


十三、Dep.trigger 的完整 trigger 分支

export function trigger(target, type, key?, newValue?, oldValue?, oldTarget?) {
  const depsMap = targetMap.get(target)
  if (!depsMap) { globalVersion++; return }  // 从未被追踪

  const run = (dep: Dep | undefined) => dep && dep.trigger()

  startBatch()
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(run)    // 清空集合,触发所有 dep
  } else {
    const isArrayTarget = isArray(target)
    const isArrayIndex = isArrayTarget && isIntegerKey(key)

    if (isArrayTarget && key === 'length') {
      // 数组 length 减小:触发所有 key >= newLength 的 dep
      const newLength = Number(newValue)
      depsMap.forEach((dep, k) => {
        if (k === 'length' || k === ARRAY_ITERATE_KEY || (!isSymbol(k) && k >= newLength)) run(dep)
      })
    } else {
      if (key !== void 0 || depsMap.has(void 0)) run(depsMap.get(key))
      if (isArrayIndex) run(depsMap.get(ARRAY_ITERATE_KEY))  // 数组索引变化触发迭代 dep

      switch (type) {
        case TriggerOpTypes.ADD:
          if (!isArrayTarget) run(depsMap.get(ITERATE_KEY))          // 对象新增属性
          if (isMap(target)) run(depsMap.get(MAP_KEY_ITERATE_KEY))   // Map 新 key,触发 keys()
          if (isArrayIndex) run(depsMap.get('length'))                // 数组新索引,触发 length dep
          break
        case TriggerOpTypes.DELETE:
          if (!isArrayTarget) run(depsMap.get(ITERATE_KEY))
          if (isMap(target)) run(depsMap.get(MAP_KEY_ITERATE_KEY))
          break
        case TriggerOpTypes.SET:
          if (isMap(target)) run(depsMap.get(ITERATE_KEY))   // Map.set,触发 forEach/values/entries
          break
      }
    }
  }
  endBatch()
}

十四、完整架构图

┌─────────────────────────────────────────────────────────────────┐
│                         响应式数据层                              │
│                                                                  │
│  ref(RefImpl)                  reactive(Proxy)               │
│  · _rawValue 存原始值             · baseHandlers(Object/Array)  │
│  · .value getter/setter          · collectionHandlers(Map/Set) │
│  · 对象内部用 toReactive()        · 懒代理(访问时才 reactive)    │
│  · createRef 去重                 · 四张 WeakMap 缓存             │
│                                                                  │
│  computed(ComputedRefImpl)                                     │
│  · 既是 Subscriber 又是 Dep                                      │
│  · DIRTY flag + globalVersion 快速路径                           │
│  · dep.version 控制下游是否重算                                   │
└──────────────────────────────┬──────────────────────────────────┘

                    Dep(双向链表 Link)
                    track() / trigger()
                    ITERATE_KEY / ARRAY_ITERATE_KEY / MAP_KEY_ITERATE_KEY

┌──────────────────────────────▼──────────────────────────────────┐
│                      ReactiveEffect 层                            │
│                                                                  │
│  watchEffect  → eager · fn=副作用 · scheduler=queueJob(pre)     │
│  watch        → lazy · fn=getter · cb=副作用 · forceTrigger      │
│               · INITIAL_WATCHER_VALUE · once · cleanupMap       │
│  computed     → lazy · 读时算 · 有缓存 · 也是 Dep               │
│  组件渲染     → eager · fn=renderComponentRoot · queueJob        │
│                                                                  │
│  EffectFlags:ACTIVE/RUNNING/TRACKING/NOTIFIED/DIRTY/           │
│               ALLOW_RECURSE/PAUSED/EVALUATED                    │
│  prepareDeps/cleanupDeps:条件分支依赖精确清除                   │
│  batch/endBatch:batchedComputed 先于 batchedSub                │
└──────────────────────────────┬──────────────────────────────────┘
                               │ scheduler
┌──────────────────────────────▼──────────────────────────────────┐
│                         调度层                                    │
│  queueJob → Promise.resolve().then(flushJobs)                   │
│  pre flush → 组件更新(父→子)→ post flush                      │
└──────────────────────────────┬──────────────────────────────────┘
                               │ stop()
┌──────────────────────────────▼──────────────────────────────────┐
│                       EffectScope 层                              │
│  每个组件一个 scope · stop() 批量停止所有 effect                  │
│  嵌套 scope · detached scope · O(1) 移除算法                    │
│  on()/off() 嵌套计数 · pause()/resume() 级联                    │
│  onScopeDispose 替代 onUnmounted 让 composable 可脱离组件使用   │
└─────────────────────────────────────────────────────────────────┘

一句话总结每层

文件 核心职责
constants.ts ReactiveFlags 枚举 + TrackOpTypes + TriggerOpTypes
dep.ts Dep 类 + Link 双向链表 + globalVersion + track/trigger 函数 + 三个 ITERATE_KEY
effect.ts ReactiveEffect + EffectFlags + prepareDeps/cleanupDeps + batch/endBatch
reactive.ts createReactiveObject + 四张 WeakMap + ReactiveFlags 工具函数
baseHandlers.ts Object/Array 的 Proxy handler,懒代理,ref 解包,hasOwnProperty patch
arrayInstrumentations.ts searchProxy / noTracking / reactiveReadArray + 所有数组方法 patch
collectionHandlers.ts createInstrumentations 工厂 + Map/Set 方法 patch + 四种组合 handler
ref.ts RefImpl + shallowRef + triggerRef + customRef + proxyRefs + ObjectRefImpl + GetterRefImpl + toRef/toRefs
computed.ts ComputedRefImpl + refreshComputed + globalVersion 快速路径
watch.ts baseWatch + traverse + onWatcherCleanup + cleanupMap + WatchHandle
effectScope.ts EffectScope + on/off + pause/resume + O(1) 移除 + onScopeDispose

核心结论

一条主线:

所有响应式消费者(watchEffect/watch/computed/组件渲染)都是 ReactiveEffect 的变体。scheduler 决定触发方式,lazy/eager 决定首次执行时机,dep 决定是否作为上游。

数据结构演进:

Vue 2         → Set<Watcher>(per property,defineProperty)
Vue 3.0–3.4  → WeakMap → Map → Set(三层,两份存储,1426KB)
Vue 3.5       → 双向链表 Link(一份,631KB,-56%,版本计数)

响应式系统的本质:

建立“数据”和“副作用”之间的追踪关系,当数据变化时用最小代价找到需要更新的副作用并执行。从 defineProperty 到双向链表,每一代都在让追踪更精确、边界更少、内存更省。


源码:vuejs/core main 分支 packages/reactivity/src/(共 11 文件,3863 行)
参考:PR #10397 · Vue 3.5 官方博客 · 《Vue.js 设计与实现》(霍春阳)