Vue3 的响应式系统源码级原始值类型自动装箱与拆箱(ref 的 .value 访问机制)实现原理
1. 问题描述
在 Vue3 的响应式系统中,ref 不仅可以处理对象,还可以处理原始值(如数字、字符串、布尔值)。原始值在 JavaScript 中是不可变的,无法像对象那样被 Proxy 拦截。Vue3 如何实现原始值的响应式?为什么通过 ref 创建的响应式原始值需要通过 .value 访问?其内部的自动“装箱”和“拆箱”机制是如何实现的?
2. 核心概念:原始值的响应式困境
- 原始值:
number、string、boolean、null、undefined、symbol、bigint。 - 问题:Proxy 只能代理对象,无法直接代理原始值。若直接将原始值传给
reactive(),会直接返回原值,无法追踪变化。
3. 解决思路:对象包装(装箱)
Vue3 的解决方案是:将原始值包装成一个具有 value 属性的普通对象,然后对这个对象进行响应式处理。这个过程称为“装箱”。
// 伪代码示意
function ref(rawValue) {
// 装箱:将原始值包装成 { value: rawValue }
const refObject = {
value: rawValue
}
// 对这个对象进行响应式处理
return reactive(refObject)
}
这样,对 .value 的读写就能被 Proxy 拦截,从而实现依赖收集和触发更新。
4. 源码级实现解析
我们深入到 packages/reactivity/src/ref.ts 中看 ref 的实现。
步骤 1:创建 Ref 对象
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true // 标识这是一个 ref
constructor(private _rawValue: T, public readonly __v_isShallow?: boolean) {
// 如果是浅 ref,不进行深度响应式转换;否则,如果是对象,转为 reactive
this._value = __v_isShallow ? _rawValue : convert(_rawValue)
}
get value() {
// 依赖收集
track(this, TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// 使用 hasChanged 判断值是否真的变化(处理 NaN 等情况)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 转换新值:如果是对象,转为 reactive
this._value = this.__v_isShallow ? newVal : convert(newVal)
// 触发更新
trigger(this, TriggerOpTypes.SET, 'value', newVal)
}
}
}
步骤 2:convert 函数处理对象转换
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
- 如果
ref接收的是对象,其.value会被reactive()包裹,成为深层响应式。 - 如果接收的是原始值,
convert直接返回原始值,但此时原始值已被包装在RefImpl实例的_value中。
步骤 3:ref 工厂函数
function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
5. 自动拆箱(Unwrap)机制
在模板和 reactive 中,我们无需写 .value 即可访问 ref 的值,这是如何实现的?
5.1 模板中的自动拆箱
在编译阶段,模板中的 ref 会被自动加上 .value:
<template>
<div>{{ count }}</div> <!-- 编译为 count.value -->
</template>
5.2 reactive 中的自动拆箱
在 reactive 对象中访问 ref 属性时,也会自动拆箱。这是通过 Proxy 的 get 拦截器实现的。
在 packages/reactivity/src/baseHandlers.ts 的 createGetter 中:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// ... 其他逻辑
const res = Reflect.get(target, key, receiver)
// 关键:如果获取的值是一个 ref,则返回其 .value(自动拆箱)
if (isRef(res)) {
// 如果是数组或原始值,直接拆箱;如果是对象,且不是只读,会进行依赖收集
return res.value
}
// 如果是对象,且不是浅层,转为响应式
if (isObject(res)) {
return reactive(res)
}
return res
}
}
注意:自动拆箱只发生在嵌套于 reactive 对象中,或模板编译阶段。在 JavaScript 中直接使用 ref 时,仍需手动 .value。
6. 特殊 API:toRef 与 toRefs
这两个工具函数也与自动拆箱相关,常用于保持响应式链接。
6.1 toRef
将响应式对象的某个属性转换为一个 ref,保持与源属性的同步:
function toRef(object: any, key: string) {
return new ObjectRefImpl(object, key)
}
class ObjectRefImpl {
constructor(private readonly _object, private readonly _key) {}
get value() {
return this._object[this._key] // 触发原对象的 getter
}
set value(newVal) {
this._object[this._key] = newVal // 触发原对象的 setter
}
}
toRef 创建的 ref 是“链接式”的,修改其 .value 会直接影响原对象。
6.2 toRefs
将响应式对象的每个属性都转为 ref,常用于解构不丢失响应性:
function toRefs(object) {
const ret: any = {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
7. 总结
- 装箱:
ref将原始值包装为{ value: rawValue }对象,并用RefImpl实例代理,通过 getter/setter 拦截.value访问实现响应式。 - 拆箱:在模板和
reactive对象中,Vue 自动调用.value获取ref的实际值,提供语法便利。 - 链接:
toRef/toRefs创建的ref与源对象属性保持链接,修改会同步。
该机制使得 Vue3 能够统一处理原始值和对象的响应式,同时保持 API 的简洁性。