深入 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 不再是“黑盒”,而是能窥见其优雅的内部构造。
