Vue 响应式原理完全指南:从 defineProperty 到 Alien Signals
基于 vuejs/core reactivity 包全部 11 个源码文件,彻底拆解 Vue 响应式系统每一层实现——ReactiveFlags、懒代理、数组 hack、collectionHandlers、RefImpl、双向链表、版本计数、computed 双重身份、watch/watchEffect 万物皆 effect、effectScope 生命周期管理
核心结论先放这里:
ref、reactive、computed、watch、watchEffect、组件渲染——底层全是围绕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] = x 和 arr.length = 0 都不触发。只能 monkey-patch push/pop/splice 等 7 个方法,覆盖不完整。
死穴三:初始化递归开销。 observe(data) 在组件创建时递归遍历整个 data 对象,data 越深越慢。
Vue 2 的 Watcher 是 Vue 3 ReactiveEffect 的前身,computed、watch、组件渲染都是不同配置的 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.ts 的 get 拦截里,读到嵌套对象时才按需创建 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等):用shallowReadArraytrackARRAY_ITERATE_KEY,回调里的 item 用toWrapped包成响应式 concat:调reactiveReadArray(track ARRAY_ITERATE_KEY + 返回 raw 数组的响应式映射)reduce/reduceRight:单独的reduce函数,处理初始值也需要 wrap 的 edge casetoReversed/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,不用重算
Link 节点:双向链表的基本单元
每条依赖边(“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-core 的 watch/watchEffect/watchPostEffect/watchSyncEffect 都在它之上通过 scheduler 和 augmentJob 参数扩展。
两个全局变量
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-core 的 apiWatch.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 设计与实现》(霍春阳)