Appearance
reactive 与 readonly:响应式对象是如何被代理的?
Vue 3 的响应式系统是其核心:当我们修改一个数据时,视图就自动更新。这种响应式的系统在 Vue 3 中被重写,它放弃了 Vue 2 的 Object.defineProperty,转而使用 ES6 的 Proxy。
这不仅是一次内部实现的替换,更是一次设计思想上的升级。本节,我们将深入 reactive 和 readonly 的源码,分析这个代理机制的实现原理。
为什么选择 Proxy?一场“监控”的革命
在 Vue 2 中,Object.defineProperty 来实现响应式,但这存在一些固有的技术局限性:
javascript
// Vue 2 的响应式
Object.defineProperty(obj, 'a', {
get() { /* ... */ },
set() { /* ... */ }
})这个非常管用,但有致命缺陷:
- 无法监测属性的添加”:对于后续添加的属性(如
obj.b = 2),你必须手动将其转换为响应式(Vue.set)。 - 无法监测属性的删除”:你无法检测到
delete obj.a。 - 数组的局限性:你无法监控
arr[0] = 4这种索引访问,Vue 2 只能通过“重写”push,pop等七个数组方法来妥协。
而 Proxy 则完全不同。它在目标对象之上架设了一个拦截层。。
javascript
// Vue 3 的“万能门卫”
const p = new Proxy(obj, {
get() { /* ... */ },
set() { /* ... */ },
deleteProperty() { /* ... */ },
has() { /* ... */ }
// ...拦截 13 种操作
})Proxy 可以拦截目标对象上的所有基本操作。无论是添加新属性、删除属性还是修改数组索引,都会被 Proxy 的 handler(处理器)捕获。
这从根本上解决了 Vue 2 响应式系统的所有局限性。
createReactiveObject:响应式对象的创建流程
当我们调用 reactive(obj) 时,我们并不是在直接创建 Proxy。Vue 在背后调用了一个核心工厂函数:createReactiveObject。
这个函数非常严谨,它不是直接创建代理,而是进行了一系列精密的检查和决策:
typescript
// core/packages/reactivity/src/reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>, // 代理的“规则手册”
collectionHandlers: ProxyHandler<any>, // 针对 Map/Set 的特殊规则
proxyMap: WeakMap<Target, any>, // “访客登记表”
) {
// 1. 合法性检查:只能代理对象和数组
if (!isObject(target)) {
return target
}
// 2. 防御:如果 target 已经是 Proxy,直接返回
// (除非是 readonly 想代理一个 reactive)
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 3. 缓存检查:这个“代理”是否已经存在?
// 核心!避免对同一个对象重复创建代理
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 4. 合法性检查:是否是 VUE_SKIP 或不可扩展对象?
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 5. 创建!
const proxy = new Proxy(
target,
// 普通对象用 baseHandlers,Map/Set 用 collectionHandlers
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
// 6. 登记缓存
proxyMap.set(target, proxy)
return proxy
}这个流程展示了 Vue 3 响应式系统的健壮性和高性能。其中最精妙的是第 3 步。
WeakMap 缓存:避免重复代理
想象一下,如果你在一个函数里调用了 reactive(obj),在另一个函数里又调用了一次 reactive(obj),Vue 应该创建两个不同的“代理”吗?
绝对不行。这会导致 reactive(obj) === reactive(obj) 的结果为 false,引发灾难性的 bug。
Vue 的解决方案是使用 WeakMap 作为缓存。
typescript
// core/packages/reactivity/src/reactive.ts
export const reactiveMap: WeakMap<Target, any> = new WeakMap()
export const readonlyMap: WeakMap<Target, any> = new WeakMap()
// ... 还有 shallowReactiveMap 和 shallowReadonlyMap- 键(Key):原始的
target对象。 - 值(Value):已经创建好的
proxy对象。
当 createReactiveObject 运行时,它会先查这个缓存表(proxyMap.get(target))。如果查到了,就直接返回,保证了同一个原始对象永远只对应唯一一个代理。
为什么用 WeakMap 而不是 Map?WeakMap 对键是“弱引用”。如果你的原始 target 对象在别处被销毁、垃圾回收了,WeakMap 中的缓存记录也会自动消失,从而防止内存泄漏。
handlers:Proxy 的拦截逻辑
Proxy 的第二个参数 handlers 是定义拦截逻辑的地方。Vue 3 定义了四套 handlers:
mutableHandlers:用于reactive()(可读可写)。readonlyHandlers:用于readonly()(只读)。shallowReactiveHandlers:用于shallowReactive()。shallowReadonlyHandlers:用于shallowReadonly()。
reactive 和 readonly 的 handlers 差异主要体现在 get 和 set 拦截上。
mutableHandlers (可读可写处理器)
这是 reactive 使用的核心处理器,我们重点看 get 和 set。
get(target, key, receiver):读取拦截
当代码执行 state.user 时,get 处理器被触发:
- 内部标记检查:检查
key是否是ReactiveFlags.IS_REACTIVE或ReactiveFlags.RAW等内部标记。如果是,返回相应的值(例如true或target本身)。 - 执行读取:
const res = Reflect.get(target, key, receiver)。获取原始值。 - 依赖收集:
track(target, TrackOpTypes.GET, key)。 这是关键:调用track函数,通知响应式系统:“有人正在读取这个属性,请记录下来。”(track的实现我们将在 3.3 节分析)。 - 【核心】按需(Lazy)递归代理:typescript这是 Vue 3 的一项重要性能优化。
if (isObject(res)) { // 如果读取到的值 (res) 也是一个对象, // 则递归地将其也转换为 reactive (或 readonly) return isReadonly ? readonly(res) : reactive(res) }reactive并不会在创建时立即递归代理所有嵌套对象。它只在嵌套对象被第一次访问(get)时,才“按需”将其转换为reactive。
set(target, key, value, receiver):写入拦截
当代码执行 state.count = 1 时,set 处理器被触发:
- 执行写入:
const result = Reflect.set(target, key, value, receiver)。先让原始的写入操作生效。 - 派发更新:
trigger(target, TriggerOpTypes.SET, key, value, oldValue)。 这是关键:调用trigger函数,通知响应式系统:“这个属性被修改了,请通知所有订阅者更新。”(trigger的实现我们将在 3.3 节分析)。 - 返回
result(写入是否成功)。
readonlyHandlers (只读处理器)
readonly 的处理器逻辑很简单。它的 get 处理器与 mutableHandlers 类似(同样需要 track 和递归 readonly(res))。
但它的 set 和 deleteProperty 处理器则会“拦截”所有写入和删除操作:
typescript
// core/packages/reactivity/src/baseHandlers.ts
class ReadonlyReactiveHandler extends BaseReactiveHandler {
set(target: object, key: string | symbol) {
if (__DEV__) {
// 在开发环境,发出警告
warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target,
)
}
return true // 操作静默失败
}
deleteProperty(target: object, key: string | symbol) {
if (__DEV__) {
warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target,
)
}
return true // 操作静默失败
}
}集合类型的特殊处理 (Map/Set)
Proxy 无法拦截 map.get() 或 map.set() 这种方法调用的内部执行。
因此,Vue 3 专门为 Map、Set、WeakMap、WeakSet 提供了 collectionHandlers。它不拦截 get 和 set,而是重写了 get、set、delete、has、forEach 等方法。
当 collectionHandlers 拦截到对 map.get(key) 的方法调用时,它会:
- 正常执行
target.get(key)获取value。 - 手动调用
track()收集依赖。 - 如果
value是对象,也递归地将其变为reactive或readonly。 map.set(key, value)同理,会在执行后手动调用trigger()。
总结
reactive 和 readonly 的实现是 Vue 3 响应式系统的基石。它围绕 Proxy 构建了一个健壮且高效的代理系统:
Proxy替代:使用Proxy提供了完整的 13 种操作拦截能力,解决了Object.defineProperty的所有局限性。createReactiveObject(工厂):作为统一入口,负责处理类型检查和缓存。WeakMap(缓存):通过WeakMap缓存已创建的代理,保证了对象引用的唯一性,并防止内存泄漏。Handlers(处理器):- 在
get拦截中:调用track()(收集依赖),并按需递归处理嵌套对象(性能优化)。 - 在
set拦截中:调用trigger()(派发更新)。 readonly处理器则会阻止set和deleteProperty操作。
- 在
