Vue3 的响应式系统源码级原始值响应式包装与类型转换原理
字数 1207 2025-11-23 01:20:05
Vue3 的响应式系统源码级原始值响应式包装与类型转换原理
1. 问题背景
Vue3 的响应式系统基于 Proxy 实现,但 Proxy 只能代理对象,无法直接代理原始值(如字符串、数字等)。而实际开发中,原始值也需要响应式能力。例如:
const count = ref(0); // 原始值 0 被包装为响应式
问题:Vue3 如何实现原始值的响应式?如何保证原始值在模板和响应式 API 中能正确工作?
2. 核心思路:Ref 的包装机制
Vue3 通过 ref 函数将原始值包装为一个带有 value 属性的响应式对象。核心步骤:
- 创建 Ref 对象:将原始值包装为
{ value: ... }的结构。 - 响应式化:对
value属性进行响应式处理(通过reactive或直接劫持)。 - 类型转换:在模板和响应式 API 中自动脱 ref(无需手动调用
.value)。
3. 源码级实现解析
3.1 Ref 构造函数
源码位置:packages/reactivity/src/ref.ts
class RefImpl<T> {
private _value: T
private _rawValue: T // 存储原始值,用于对比
public readonly __v_isRef = true // 标识为 ref
constructor(value: T, public readonly _shallow = false) {
this._rawValue = value
// 如果是浅层 ref,直接存储值;否则用 reactive 转换
this._value = _shallow ? value : toReactive(value)
}
get value() {
track(this, TrackOpTypes.GET, 'value') // 依赖收集
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
trigger(this, TriggerOpTypes.SET, 'value', newVal) // 触发更新
}
}
}
关键点:
toReactive(value)会判断值类型:如果是对象,调用reactive;如果是原始值,直接返回。hasChanged通过Object.is对比新旧值,避免不必要的更新。
3.2 自动类型转换(脱 Ref)
在模板和响应式 API 中,Vue3 会自动脱 ref(无需写 .value)。实现原理:
-
模板中的脱 Ref:
- 编译阶段,模板中的
ref变量会被自动解包。例如:<div>{{ count }}</div> <!-- 编译为 count.value --> - 源码位置:
packages/compiler-core/src/transforms/transformElement.ts中的processExpression逻辑。
- 编译阶段,模板中的
-
响应式 API 中的脱 Ref:
reactive包裹ref时,访问ref属性会自动解包:const count = ref(0) const obj = reactive({ count }) console.log(obj.count) // 直接输出 0,而非 Ref 对象- 源码实现:
packages/reactivity/src/reactive.ts中的get陷阱:if (isRef(res)) { return res.value // 自动解包 }
4. 特殊场景处理
4.1 嵌套 Ref 的处理
若 ref 包裹的值已经是 ref,则直接返回内部 ref,避免多层包装:
const count = ref(0)
const doubleCount = ref(count) // doubleCount 与 count 是同一个 ref
源码通过 isRef 检查实现:
export function ref(value?: unknown) {
if (isRef(value)) return value // 直接返回已存在的 ref
return new RefImpl(value)
}
4.2 响应式对象赋给 Ref
当 ref 的 value 被赋值为响应式对象时,会直接存储其原始值(而非响应式代理):
const obj = reactive({ foo: 1 })
const objRef = ref(obj)
console.log(objRef.value === obj) // true,因为存储的是原始对象
源码通过 toReactive 中的 toRaw 转换实现:
const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
5. 总结
- 原始值响应式:通过
ref包装为{ value: ... }对象,并对value属性劫持。 - 自动脱 Ref:模板编译和
reactive的 getter 中自动解包ref.value。 - 性能优化:通过
hasChanged避免重复触发更新,嵌套ref直接返回已有引用。