Vue3 的响应式系统源码级 reactive 对引用类型与原始值的不同处理策略与深层响应式转换机制
字数 1841 2025-12-11 01:14:44
Vue3 的响应式系统源码级 reactive 对引用类型与原始值的不同处理策略与深层响应式转换机制
一、题目描述
在 Vue3 的响应式系统中,reactive 与 ref 是两个核心的响应式 API。本题深入探讨 reactive API 在源码层面如何处理不同类型的数据:为什么 reactive 只能用于对象类型(Object、Array、Map、Set),而不能直接处理原始值(string、number、boolean 等)?同时解析其深层响应式转换的实现机制。
二、核心原理概述
reactive 的核心是基于 ES6 Proxy 的代理机制,Proxy 只能代理对象类型(引用类型),无法直接代理原始值。Vue3 通过 ref 来包装原始值,使其变为响应式。reactive 会递归地将对象的所有嵌套属性转换为响应式,这一过程称为“深层响应式转换”。
三、源码级逐步解析
第一步:reactive 的入口与类型判断
在 packages/reactivity/src/reactive.ts 中,reactive 函数首先检查传入值的类型:
export function reactive(target: object) {
// 如果 target 已经是只读代理,直接返回
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
关键点:
- 参数类型限制:
target: object类型声明已经限制只能传入对象类型。 - 原始值处理:如果传入原始值,TypeScript 会在编译时报错,运行时也会因为 Proxy 无法代理而抛出错误。
第二步:创建响应式对象的核心函数
createReactiveObject 函数执行以下逻辑:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 1. 如果不是对象,直接返回(开发环境会警告)
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 2. 如果已经是代理且类型匹配,直接返回缓存
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 3. 只有特定类型才能被响应式化
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 4. 创建代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
第三步:类型判断与分类处理
getTargetType 函数将目标分为三类:
enum TargetType {
INVALID = 0, // 无效类型
COMMON = 1, // 普通对象/数组
COLLECTION = 2 // Map、Set、WeakMap、WeakSet
}
function getTargetType(value: Target) {
// 标记为不可响应式的对象(如带有 __v_skip 标记)
if (value[ReactiveFlags.SKIP] || !Object.isExtensible(value)) {
return TargetType.INVALID
}
// 判断是否为集合类型
if (isMap(value) || isSet(value) || isWeakMap(value) || isWeakSet(value)) {
return TargetType.COLLECTION
}
return TargetType.COMMON
}
关键点:
- 原始值:在
isObject检查时就会被过滤,isObject函数返回false。 - 集合类型:使用不同的处理器
collectionHandlers,因为 Map/Set 的操作方法(如 get/set/add)需要特殊拦截。
第四步:深层响应式转换的实现
在 packages/reactivity/src/baseHandlers.ts 中,get 拦截器负责属性的读取:
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ... 处理内置属性(如 __v_isReactive)
const res = Reflect.get(target, key, receiver)
// 如果是内置 Symbol 或不可追踪的 key,直接返回
if (isSymbol(key) && builtInSymbols.has(key) || key === ReactiveFlags.IS_REACTIVE) {
return res
}
// 只读对象不收集依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 浅层响应式直接返回值
if (shallow) {
return res
}
// 深层响应式转换的关键:如果值是对象,递归调用 reactive
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
深层转换的触发时机:
- 惰性转换:只有当属性被访问时,才会递归转换,而不是在创建代理时一次性转换所有嵌套属性,这提高了初始性能。
- 条件判断:
isObject(res)检查值是否为对象类型,如果是则调用reactive(res)或readonly(res)。 - 缓存机制:
reactive函数内部有proxyMap缓存,同一对象不会重复代理。
第五步:原始值的处理策略
对于原始值,Vue3 提供了 ref API:
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
constructor(value: T, public readonly __v_isShallow: boolean) {
// 如果是对象,用 reactive 包装
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
track(this, TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// ... 触发更新
}
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
关键设计:
- 包装对象:
ref将原始值包装在{ value: ... }对象中,使其可以被 Proxy 代理。 - 自动解包:在模板中和
reactive对象中访问ref时,会自动解包.value。 - 对象处理:如果
ref的值是对象,内部会调用reactive进行深层转换。
四、设计原理总结
- Proxy 的限制:Proxy 只能代理对象,这是语言层面的限制,不是 Vue 的设计缺陷。
- 类型安全:通过 TypeScript 类型声明和运行时检查,提前发现错误。
- 性能优化:
- 惰性转换:按需转换嵌套属性
- 缓存机制:避免重复代理
- 集合类型特殊处理:优化 Map/Set 性能
- API 职责分离:
reactive:处理对象类型,提供深层响应式ref:处理原始值,也支持对象(内部用 reactive)shallowReactive:提供浅层响应式,不递归转换
五、面试回答要点
当被问到“为什么 reactive 不能处理原始值”时,可以这样回答:
- 根本原因:ES6 Proxy 的机制限制,只能代理对象类型。
- 解决方案:使用
ref包装原始值,通过.value访问。 - 深层转换原理:
reactive在 getter 中递归调用自身,实现惰性深层响应式。 - 性能考虑:按需转换 + 缓存机制,避免不必要的性能开销。
- 类型设计:通过 TypeScript 类型系统提供编译时检查,提高代码健壮性。
这种设计既遵循了 JavaScript 语言特性,又通过合理的 API 分工提供了完整且高效的响应式解决方案。