Vue 3 响应式原理源码全解析:从 Proxy 到 computed/watch 的完整实现
一、写在开头
前端框架的核心任务是将数据映射为 DOM,并在数据变化时自动更新视图。响应式系统正是实现这一自动更新的基础——当应用状态改变时,依赖该状态的视图、计算属性、侦听器等能够自动重新执行。
Vue 2 使用 Object.defineProperty 劫持对象属性的 getter/setter,配合观察者模式实现了响应式。但它存在几个难以逾越的局限:无法检测属性的添加或删除;对数组的变异方法需要额外处理;初始化时需递归遍历所有属性,存在性能开销。
Vue 3 基于 ES2015 的 Proxy 实现了全新的响应式系统,不仅完美解决了上述问题,还带来了更好的性能、更简洁的代码结构,同时支持 Map、Set 等复杂数据类型。本文将结合 packages/reactivity 目录下的源码,深度剖析这一系统的设计与实现,并手写一个 mini 版巩固理解。
二、响应式系统的核心:Proxy vs Object.defineProperty
2.1 Object.defineProperty 的工作原理
Object.defineProperty(obj, key, descriptor) 可以精确控制对象属性的读取和赋值行为:
javascript
let obj = {}
let val = 1
Object.defineProperty(obj, 'a', {
get() {
console.log('get a')
return val
},
set(newVal) {
console.log('set a')
val = newVal
}
})
obj.a // 触发 get
obj.a = 2 // 触发 set
Vue 2 通过递归遍历对象的所有属性,为每个属性添加 getter 和 setter。在 getter 中收集依赖(Watcher),setter 中触发更新。但这种方式存在明显局限:
-
无法监听属性新增/删除:
obj.newKey = 'value'不会触发 setter,因此 Vue 2 提供了Vue.set/delete。 -
无法直接监听数组索引和 length 变化:需要重写数组的 7 种变异方法(push/pop/shift/unshift/splice/sort/reverse)。
-
初始化时需要深度递归,影响启动性能,且对于深层嵌套的对象可能造成不必要的观察。
简单模拟 Vue 2 响应式:
javascript
function defineReactive(obj, key) {
let val = obj[key]
const dep = [] // 依赖列表(简化)
Object.defineProperty(obj, key, {
get() {
if (currentEffect) dep.push(currentEffect)
return val
},
set(newVal) {
val = newVal
dep.forEach(fn => fn())
}
})
}
// 需要递归遍历
function observe(obj) {
Object.keys(obj).forEach(key => defineReactive(obj, key))
}
2.2 Proxy 的工作原理
Proxy 可以创建一个对象的代理,拦截几乎所有基本操作:
javascript
const p = new Proxy(target, {
get(target, key, receiver) { /* ... */ },
set(target, key, value, receiver) { /* ... */ },
deleteProperty(target, key) { /* ... */ },
// 共支持13种拦截方法
})
Vue 3 利用 Proxy 的 get、set、deleteProperty、has、ownKeys 等拦截器,能够全面捕获对数据的操作。其优势包括:
-
可监听属性的增删:
set拦截器可感知新增属性,deleteProperty拦截删除操作。 -
自然支持数组:直接拦截
arr[0]、arr.length等操作,无需重写数组方法。 -
懒代理:只在读取某个属性且其值是对象时才进行递归代理,提升初始化性能。
-
可代理更多数据类型:Map、Set、WeakMap、WeakSet 等。
简单 Proxy 示例:
javascript
const handler = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler) // 懒代理
}
return target[key]
},
set(target, key, value) {
target[key] = value
console.log('更新视图')
return true
}
}
const data = new Proxy({ a: 1, arr: [1,2,3] }, handler)
data.a = 2 // 触发更新
data.b = 3 // 触发更新(新增属性)
data.arr.push(4) // 触发更新
2.3 两者的详细对比
| 对比维度 | Object.defineProperty | Proxy |
|---|---|---|
| 功能完整性 | 无法监听属性增删、数组索引和 length | 可拦截13种操作,覆盖所有数据变更场景 |
| 性能 | 初始化需递归遍历,内存消耗大 | 懒代理,按需递归,整体更高效 |
| 易用性 | 需要额外 API(set/set/delete),数组特殊处理 | 统一拦截,代码简洁 |
| 兼容性 | 支持 IE9+,生态成熟 | 不支持 IE,但现代浏览器全面支持 |
| 数据结构支持 | 仅限普通对象 | 支持数组、Map、Set、WeakMap、WeakSet |
Vue 3 选择 Proxy 正是因为它提供了更完整的拦截能力和更友好的开发体验。虽然放弃了 IE 兼容性,但顺应了现代前端发展趋势,也使得响应式系统的实现代码量大幅减少。
三、Vue 3 响应式系统的整体架构
Vue 3 响应式系统的核心模块均位于 packages/reactivity/src 目录下,主要包括:
-
reactive:将普通对象转换为响应式代理
-
ref:包装基本类型值(或对象),使其变为响应式
-
effect:创建副作用函数,并在其依赖变化时自动重新执行
-
computed:基于其他响应式数据计算得出新值,具有懒执行和缓存特性
-
watch/watchEffect:侦听响应式数据的变化并执行回调
整体工作流程如下:
text
[应用数据]
│
├─ reactive(obj) / ref(val) 创建响应式数据
│
└─> effect / computed / watch 创建副作用,依赖响应式数据
│
▼
Proxy get 拦截 ──── track(收集依赖)────> 存储到 targetMap (WeakMap)
│
Proxy set/delete 拦截 ─── trigger(触发依赖)────> 取出对应 effect 重新执行
│
▼
[异步更新队列] -> 执行副作用 -> 更新 DOM
核心概念区分:
-
响应式数据:被 Proxy 代理后的对象或通过 ref 包装的值,能够被“追踪”变化。
-
副作用函数(effect):依赖响应式数据的函数,当数据变化时会被重新调用。组件的渲染函数就是典型的副作用。
整个系统围绕“依赖收集”和“触发更新”两个阶段运转,接下来我们将深入源码逐一剖析。
四、依赖收集的完整实现
4.1 依赖收集的基本原理
依赖 是指某个副作用函数读取了哪些响应式数据。当数据变化时,需要通知这些副作用重新执行。依赖收集发生在响应式数据的 get 拦截 阶段。
Vue 3 使用一个三层结构来存储依赖关系:
text
targetMap: WeakMap<target, depsMap>
depsMap: Map<key, dep>
dep: Set<ReactiveEffect>
-
targetMap是一个 WeakMap,键为原始目标对象,值为depsMap。使用 WeakMap 利于垃圾回收。 -
depsMap是一个 Map,键为目标对象的具体属性名,值为dep。 -
dep是一个 Set,存放所有与该属性关联的ReactiveEffect(副作用对象)。Set 确保依赖不重复。
4.2 核心源码分析
reactive 函数 (packages/reactivity/src/reactive.ts)
javascript
export function reactive(target) {
// 如果目标已是只读代理或响应式代理,直接返回
if (isReadonly(target)) return target
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap)
}
createReactiveObject 核心逻辑:
javascript
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 非对象类型直接返回
if (!isObject(target)) return target
// 如果已经代理过,返回缓存的代理(proxyMap 防止重复代理)
const existingProxy = proxyMap.get(target)
if (existingProxy) return existingProxy
// 根据类型选择处理器:普通对象用 baseHandlers,集合类型用 collectionHandlers
const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
Proxy 的 get 拦截器 实现在 baseHandlers 中(packages/reactivity/src/baseHandlers.ts),核心是调用 track 收集依赖:
javascript
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 处理各种内部 key(如 __v_isReactive、__v_raw 等)...
const res = Reflect.get(target, key, receiver)
// 非只读时进行依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 若值是对象且非浅层代理,递归转换为响应式(懒代理)
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
track 函数 详细解析(packages/reactivity/src/effect.ts)
javascript
export function track(target, type, key) {
// 当前激活的 effect 存在时才收集
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 将当前 effect 加入依赖集合
trackEffects(dep)
}
}
function trackEffects(dep) {
// 避免重复收集:判断 effect 的 deps 数组是否已包含此 dep
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep) // effect 也反向记录该 dep,便于清理
}
}
优化策略:
-
activeEffect确保只有当前正在执行的副作用才会被收集,避免无关读取触发收集。 -
双向记录(effect 记录它依赖的所有 dep,dep 记录所有订阅的 effect),使得 effect 重新执行时可以清理过期的依赖。
-
使用位运算
trackOpBit进行递归跟踪的控制,防止深层嵌套下的重复收集。
4.3 ref 的特殊处理
ref 用于让基本类型值(或对象)具备响应式。对象类型 ref 内部会调用 reactive 转换 .value 的值。
ref 的实现 (packages/reactivity/src/ref.ts)
javascript
export function ref(value) {
return createRef(value, false)
}
function createRef(rawValue, shallow) {
if (isRef(rawValue)) return rawValue
return new RefImpl(rawValue, shallow)
}
class RefImpl {
constructor(value, __v_isShallow) {
this._rawValue = value
this._value = __v_isShallow ? value : toReactive(value) // 对象转 reactive
this.__v_isRef = true
}
get value() {
trackRefValue(this) // 收集依赖
return this._value
}
set value(newVal) {
if (hasChanged(this._rawValue, newVal)) {
this._rawValue = newVal
this._value = toReactive(newVal)
triggerRefValue(this) // 触发更新
}
}
}
ref 依赖收集直接通过 trackRefValue 将自身作为一个整体收集,而不是按属性。其依赖存储在 dep 属性(Set)中。与 reactive 相比,ref 更轻量,适合单一值的响应式包裹,且可通过 .value 直接替换整个对象。
五、触发更新的完整实现
5.1 触发更新的基本原理
触发更新发生在响应式数据的 set 或 deleteProperty 拦截 阶段。当数据变更时,根据修改类型(新增、修改、删除)从 targetMap 中找到对应的 dep,并执行所有关联的副作用。
为提升性能,Vue 3 不会立即同步执行副作用,而是将副作用放入一个异步更新队列,在下一个 tick 统一执行,从而批量处理更新,避免不必要的重复计算。
5.2 核心源码分析
set 拦截器 (packages/reactivity/src/baseHandlers.ts)
javascript
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
// 判断操作类型:新增(ADD)还是修改(SET)
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 若 receiver 是 target 的代理且未被屏蔽,触发更新
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
trigger 函数 详细解析(packages/reactivity/src/effect.ts)
javascript
export function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
let deps = []
// 获取与 key 直接相关的依赖
if (key !== undefined) {
deps.push(depsMap.get(key))
}
// 处理 ADD / DELETE / SET 等特殊操作(如数组 length 变更、迭代器相关)
switch (type) {
case TriggerOpTypes.ADD:
// 对数组新增,需要触发 length 相关的依赖
if (isArray(target)) {
deps.push(depsMap.get('length'))
}
// ...
break
case TriggerOpTypes.DELETE:
// ...
break
case TriggerOpTypes.SET:
// ...
break
}
// 合并所有需要执行的 effect
const effects = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
// 通过调度器执行副作用(加入更新队列)
triggerEffects(createDep(effects))
}
function triggerEffects(dep) {
for (const effect of dep) {
// 若 effect 有 scheduler,则使用 scheduler 调度执行
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
不同操作类型的处理:新增属性 除了触发该属性的依赖,还会触发与迭代(如 Object.keys)相关的依赖;删除属性 同理;对于数组,修改 length 或通过索引设置可能触发多个依赖。
5.3 异步更新队列
当副作用被触发时,不会立即执行 effect.run(),而是推入一个队列 queue,并交由 scheduler 调度。这样同一个 Tick 内的多次数据变更只会触发一次副作用刷新。
nextTick 的实现(packages/runtime-core/src/scheduler.ts)
javascript
const queue = []
let isFlushing = false
const resolvedPromise = Promise.resolve()
export function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing) {
isFlushing = true
resolvedPromise.then(flushJobs)
}
}
function flushJobs() {
try {
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
job()
}
} finally {
isFlushing = false
queue.length = 0
}
}
nextTick 实际上就是 Promise.resolve().then(callback)(在 Vue 3 中优先使用微任务),等待当前宏任务执行完毕后清空队列。
批量更新的优化:
-
同一个 effect 若被多次触发,只会入队一次(通过
queue.includes判重)。 -
允许提供
scheduler,例如组件的渲染函数会使用自定义 scheduler,确保渲染过程高效批处理。
六、computed 的实现原理
6.1 computed 的基本概念和特性
计算属性接收一个 getter 函数,返回一个只读的响应式 ref 对象。它具有两大核心特性:
-
懒执行:只有在被读取时才会计算值,若没有依赖项变化,不会主动重新计算。
-
缓存:依赖项不变时多次读取会立即返回上一次的计算结果,而不会重复执行 getter。
-
可写计算属性:若提供 setter,则可通过赋值触发展更新。
6.2 核心源码分析
computed 函数 (packages/reactivity/src/computed.ts)
javascript
export function computed(getterOrOptions) {
let getter, setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = () => warn('Write operation failed: computed value is readonly')
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set)
}
ComputedRefImpl 类
javascript
class ComputedRefImpl {
constructor(getter, _setter, isReadonly) {
this._setter = _setter
this._dirty = true // 脏标记,控制是否需要重新计算
this._cacheable = true // 是否启用缓存
this.effect = new ReactiveEffect(getter, () => {
// 当依赖变化时,不立即求值,仅标记为脏,并触发依赖此 computed 的 effect 更新
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // 通知上层订阅者
}
})
}
get value() {
// 读取时进行依赖收集
const self = toRaw(this)
trackRefValue(self)
// 如果脏或不需要缓存,则重新计算
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run() // 执行 getter,此时会收集依赖
}
return self._value
}
set value(newValue) {
this._setter(newValue)
}
}
懒执行与缓存机制:
-
内部创建了一个
ReactiveEffect,但不立即执行(effect 构造时lazy选项阻止自动运行)。 -
依赖变更时,scheduler 仅将
_dirty置为true,并触发自身triggerRefValue,通知依赖该 computed 的 effect 重新执行。 -
真正求值发生在
get value()中,且只有_dirty为true时才重新执行 getter,否则直接返回_value。 -
当没有任何地方读取 computed 时,即使依赖变化也不会发生计算,这就是懒执行。
6.3 computed 与 effect 的区别
| 对比维度 | computed | effect |
|---|---|---|
| 执行时机 | 懒执行:只有被读取时才计算 | 立即执行一次,之后依赖变化时自动重新执行 |
| 返回值 | 返回一个 ref 对象,通过 .value 访问 |
返回一个 runner 函数,无自动返回值概念 |
| 缓存 | 依赖不变时缓存结果 | 每次重新执行,无缓存 |
| 主要用途 | 派生计算、模板中高效绑定 | 执行副作用,如 DOM 更新、watch 回调 |
| 内部实现 | 基于 effect,额外增加了脏检查与缓存 | 纯粹的依赖收集与重新执行 |
使用场景:当需要基于现有数据产生新数据并需要缓存时,用 computed;当需要在数据变化时执行某些操作(如异步请求、DOM 操作)时,用 effect(或 watch)。
七、watch 的实现原理
7.1 watch 的基本概念和使用方式
Vue 3 提供了 watch 和 watchEffect 两个 API:
-
watchEffect:立即执行传入的函数,并自动追踪其中的响应式依赖,依赖变化时重新执行。 -
watch:显式指定监听一个或多个数据源,数据变化时触发回调,支持懒执行、获取旧值、深度监听等。
常用配置:
-
immediate: true:创建时立即执行回调。 -
deep: true:深度监听对象内部变化。 -
flush: 'pre' | 'post' | 'sync':控制回调的执行时机。
7.2 核心源码分析
watch 函数 和 watchEffect 最终都调用 doWatch (packages/reactivity/src/watch.ts)
javascript
function watch(source, cb, options) {
return doWatch(source, cb, options)
}
export function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
doWatch 核心流程:
-
规范化监听源:
source可以是 ref、reactive 对象、getter 函数、数组等。-
若为 ref 或 reactive,会自动解包或深度追踪。
-
若为函数,视为 getter,每次运行获取当前值。
-
若为数组,组合多个源,回调参数为数组形式的新旧值。
-
-
创建 effect:根据配置创建
ReactiveEffect,scheduler中执行用户回调。
javascript
function doWatch(source, cb, { immediate, deep, flush } = {}) {
// ... 规范化 source 为 getter
let getter
if (isRef(source)) {
getter = () => source.value
} else if (isReactive(source)) {
getter = () => source
deep = true // 默认对 reactive 对象开启深度监听
} else if (isFunction(source)) {
getter = source
} // ...
// 深度监听时,在 getter 内部递归读取属性,确保触发依赖收集
if (deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let oldValue = {}
const job = () => {
if (cb) {
const newValue = effect.run()
if (deep || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup)
oldValue = newValue
}
} else {
effect.run() // watchEffect
}
}
let scheduler
if (flush === 'sync') {
scheduler = job
} else {
scheduler = () => queuePreFlushCb(job) // 默认 pre 队列,DOM 更新前执行
}
const effect = new ReactiveEffect(getter, scheduler)
if (cb) {
if (immediate) job()
else oldValue = effect.run() // 首次执行获取旧值
} else {
effect.run() // watchEffect 立即执行
}
return () => effect.stop() // 返回取消监听的函数
}
deep 实现:traverse 函数递归访问对象的所有属性,触发 getter 从而收集所有深层依赖。这样任意层级的变化都能被感知。
flush 时机处理:
-
pre:默认,回调在组件更新前执行(通过queuePreFlushCb加入 pre 队列)。 -
post:回调在组件更新后执行(通过queuePostRenderEffect)。 -
sync:同步执行,数据变化立即触发回调。
7.3 watch 与 computed 的区别
| 对比维度 | watch | computed |
|---|---|---|
| 用途 | 执行副作用(异步请求、DOM 操作) | 派生计算新值,主要用于模板绑定 |
| 执行 | 默认懒执行(immediate 可改为立即) | 懒执行,依赖不变不计算 |
| 返回值 | 返回停止监听函数 | 返回只读 ref |
| 缓存 | 无 | 有 |
| 访问新值 | 回调参数提供新旧值 | 通过 .value 获取当前计算结果 |
| 深度监听 | 支持 deep 递归追踪 | 依赖自动追踪,无需 deep |
| 典型场景 | “当某个数据变化时,发起网络请求” | “根据 firstName 和 lastName 计算 fullName” |
两者在响应式系统中分工明确:computed 用于纯计算,watch 用于有副作用的响应。
八、手写一个简化版的 Vue 3 响应式系统
我们将从零实现一个 mini 响应式系统,包括 reactive、effect、computed、watch。
8.1 整体设计思路
核心模块:
-
reactive(target):返回 Proxy 代理。 -
effect(fn):立刻执行 fn,并追踪依赖。 -
track(target, key):在 get 中调用,收集当前 effect。 -
trigger(target, key):在 set 中调用,运行对应的 effect。 -
computed(getter):返回懒执行、有缓存的 ref。 -
watch(source, cb):监听 source 变化并执行 cb。
数据结构:targetMap(WeakMap) -> depsMap(Map) -> dep(Set)。
8.2 分步实现
javascript
// mini-reactive.js
let activeEffect = null
const targetMap = new WeakMap()
// ---- track & trigger ----
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
activeEffect.deps.push(dep) // 反向记录
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// 使用新 Set 避免死循环(清理等)
const effectsToRun = new Set(dep)
effectsToRun.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
})
}
}
// ---- reactive ----
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
if (typeof res === 'object' && res !== null) {
return reactive(res) // 懒代理
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
// ---- effect ----
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn
this.scheduler = scheduler
this.deps = []
}
run() {
// 清理上次依赖,重新收集
cleanupEffect(this)
activeEffect = this
const result = this.fn()
activeEffect = null
return result
}
stop() {
cleanupEffect(this)
this.active = false
}
}
function cleanupEffect(effect) {
effect.deps.forEach(dep => dep.delete(effect))
effect.deps.length = 0
}
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
// ---- ref ----
function ref(value) {
return new RefImpl(value)
}
class RefImpl {
constructor(value) {
this._value = value
this.dep = new Set()
}
get value() {
if (activeEffect) {
this.dep.add(activeEffect)
}
return this._value
}
set value(newVal) {
if (newVal !== this._value) {
this._value = newVal
this.dep.forEach(effect => {
if (effect.scheduler) effect.scheduler()
else effect.run()
})
}
}
}
// ---- computed ----
function computed(getter) {
let dirty = true
let value
const _effect = new ReactiveEffect(getter, () => {
// scheduler:依赖变化时标记脏,不立即求值
if (!dirty) {
dirty = true
trigger(computedRef, 'value')
}
})
const computedRef = {
get value() {
track(computedRef, 'value') // 收集 computed 的依赖
if (dirty) {
dirty = false
value = _effect.run()
}
return value
}
}
return computedRef
}
// ---- watch ----
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else if (source instanceof RefImpl) {
getter = () => source.value
} else {
// 简化:假设为 reactive 对象,深度监听
getter = () => traverse(source)
}
let oldValue
const job = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
const effect = new ReactiveEffect(getter, job)
if (options.immediate) {
job()
} else {
oldValue = effect.run()
}
}
// 深度遍历依赖
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return value
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
8.3 测试与验证
javascript
// 测试 reactive 和 effect
const state = reactive({ count: 0, nested: { a: 1 } })
let dummy
effect(() => {
dummy = state.count * 2
})
console.log(dummy) // 0
state.count = 5
console.log(dummy) // 10
// 测试 computed
const countRef = ref(2)
const double = computed(() => countRef.value * 2)
console.log(double.value) // 4
countRef.value = 3
console.log(double.value) // 6(懒计算)
// 测试 watch
let oldVal, newVal
watch(() => state.count, (n, o) => {
newVal = n; oldVal = o
})
state.count = 8
console.log(oldVal, newVal) // 5 8
与官方实现的差异:
-
未处理数组 length 与迭代器相关依赖、Map/Set 的拦截。
-
没有异步更新队列(同步执行),官方使用微任务批处理。
-
未实现 effect 调度优先级、
stop时的完全清理。 -
缺少边界情况处理(如代理只读对象、避免死循环等)。
完整可运行代码及在线示例请查看:CodeSandBox 链接(读者可自行创建运行环境测试)。
九、总结与思考
Vue 3 响应式系统基于 Proxy 重新设计,核心思想仍然是依赖收集与触发更新,但实现上更加精简、高效。通过 WeakMap -> Map -> Set 三层结构管理依赖,通过懒代理提升初始化性能,通过 effect 统一管理副作用,通过 computed 和 watch 提供更上层的响应式工具。
本文核心回顾:
-
Proxy 对比 Object.defineProperty 的四大优势。
-
reactive / ref 的创建与依赖收集流程。
-
track / trigger 的源码实现与数据结构。
-
computed 的懒执行、缓存与 effect 的关系。
-
watch 的多源监听、深度追踪与回调调度。
优势与不足:
-
优势:支持更多数据结构、性能更佳、代码更简洁、无 set/set/delete 等补丁 API。
-
不足:不支持 IE,依赖 Proxy 导致 Polyfill 困难;深层嵌套大对象频繁操作时仍有优化空间。
展望: 未来响应式系统可能会引入更细粒度的调度、与编译时优化(如 Vue Vapor Mode)深度结合,甚至利用 TC39 新特性(如 Signals 提案)进一步统一状态管理。
本文通过对 Vue 3 响应式源码的逐层剖析,从设计思想到核心实现,再到手写 mini 系统,力求帮助读者建立起完整的知识图谱。欢迎在评论区分享你对响应式系统的理解或疑问,我们一起探讨!
更多推荐

所有评论(0)