深入 Vue 3 ref 的底层实现
深入 Vue 3 ref
的底层实现:从响应式基石到自动解包之谜
在 Vue 3 的 Composition API 中,ref
是我们最常使用的响应式工具之一。它看似简单,但其底层实现却蕴含了 Vue 3 响应式系统的核心思想。本文将带你深入源码,剖析 ref
的创建、依赖收集、触发更新以及自动解包的完整机制。
一、ref
的核心作用
ref
用于将一个原始值(如 string
、number
、boolean
)或对象转换为响应式数据。与 reactive
不同,ref
返回的是一个包含 .value
属性的包装对象:
import { ref } from 'vue'
const count = ref(0)
count.value++ // 修改值
二、ref
的底层实现原理
ref
的实现依赖于 Vue 3 的 reactive
系统和 effect
依赖追踪机制。其核心源码位于 packages/reactivity/src/ref.ts
。
1. 创建 ref
:createRef
当我们调用 ref(value)
时,实际上是调用了 createRef
函数:
// 简化后的源码逻辑
function createRef(rawValue: unknown, shallow = false) {
// 返回一个 RefImpl 实例
return new RefImpl(rawValue, shallow)
}
RefImpl
是 ref
的核心类:
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true // 标记这是一个 ref
constructor(private _rawValue: T, private _shallow = false) {
// 如果是对象,使用 reactive 包装(深度 ref)
this._value = _shallow ? _rawValue : reactive(_rawValue)
}
// getter: 读取 .value 时触发依赖收集
get value() {
// track 函数:收集当前活跃的 effect 作为依赖
track(this, 'get', 'value')
return this._value
}
// setter: 修改 .value 时触发依赖更新
set value(newVal) {
// 浅比较,避免不必要的更新
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 如果是对象,重新 reactive 包装
this._value = this._shallow ? newVal : reactive(newVal)
// trigger 函数:通知所有依赖更新
trigger(this, 'set', 'value', newVal)
}
}
}
关键点解析:
__v_isRef
标志:用于在模板或setup
中识别ref
,实现自动解包。reactive
包装:ref
内部对对象值使用reactive
进行深层响应式处理(除非是shallowRef
)。track
和trigger
:这是响应式系统的“心脏”。track
在读取时收集依赖(如render effect
),trigger
在修改时通知依赖重新执行。
2. 依赖收集:track
函数
当组件渲染时访问 count.value
,会触发 RefImpl
的 get value()
。此时 track(this, 'get', 'value')
会被调用:
// track 函数简化逻辑
function track(target: RefImpl, type: TrackOpTypes, key: string) {
// 获取当前正在执行的 effect(如组件的 render effect)
const effect = activeEffect
if (effect) {
// 将 effect 添加到 target 的依赖集合中
// 依赖存储在 WeakMap<target, Map<key, Set<effect>>> 中
trackEffects(getDepFromTarget(target, key))
}
}
这确保了当 ref
值变化时,所有依赖它的 effect
(通常是组件渲染函数)都会被重新执行。
3. 触发更新:trigger
函数
当 count.value = 1
时,set value()
被调用,trigger(this, 'set', 'value', newVal)
执行:
function trigger(target: RefImpl, type: TriggerOpTypes, key: string, newValue: any) {
// 从依赖映射中取出所有依赖此 key 的 effects
const deps = getDepFromTarget(target, key)
// 遍历并执行这些 effects
triggerEffects(deps)
}
这会调度所有依赖该 ref
的 effect
重新运行,从而更新视图。
三、ref
的自动解包(Auto-unwrapping)
这是 ref
最“神奇”的特性之一:在模板或 setup
返回的对象中,我们可以直接使用 count
而非 count.value
。
<template>
<!-- 模板中直接使用 count,无需 .value -->
<div>{{ count }}</div>
<button @click="count++">+</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 在 script 中仍需 count.value
</script>
实现原理:
自动解包发生在 模板编译阶段 和 响应式代理的 get
拦截器中。
模板编译:Vue 的模板编译器会静态分析模板,识别出变量名(如
count
)并生成访问.value
的代码:// 编译后类似 () => _ctx.count.value
响应式代理拦截(关键):即使在
setup
返回的对象中,ref
也会被自动解包。这得益于reactive
代理的get
拦截器:// reactive 代理的 get 拦截器 const get = (target, key, receiver) => { const res = Reflect.get(target, key, receiver) // 如果获取的值是 ref,且 key 不是 ref 的自有属性 if (isRef(res) && !isObject(res)) { // 自动返回 .value(解包) return res.value } return res }
因此,当你在模板或
setup
返回的对象中访问state.count
(假设state
是reactive
对象,count
是ref
),代理会自动返回count.value
。
四、shallowRef
与 triggerRef
shallowRef
:创建一个“浅层”ref
,其.value
不会被reactive
包装。适用于大型对象或不可变数据,避免深度响应式带来的性能开销。triggerRef
:手动触发shallowRef
的更新。因为shallowRef
的.value
是普通对象,修改其内部属性不会自动触发trigger
,需手动调用triggerRef(myShallowRef)
。
五、总结
ref
的底层实现精妙地结合了:
- 类封装:
RefImpl
管理值和响应式逻辑。 - 依赖追踪:通过
track
/trigger
与effect
系统联动。 - 自动解包:编译时优化 + 运行时代理拦截,提升开发体验。
理解 ref
的实现,不仅有助于我们更好地使用 Vue 3,更能深入掌握其响应式设计的哲学:以最小的侵入性,实现最大的灵活性。
小提示:在
script setup
中,ref
的.value
在模板中自动解包,但在watch
、computed
等函数中仍需手动访问.value
。这是由其运行时机制决定的。
通过本文,希望你对 ref
不再是“黑盒”,而是能窥见其优雅的内部构造。