05-ref的精妙设计
本章目标:深入理解 ref 的实现原理与解包机制
上一章我们深入了 track/trigger 机制,理解了依赖收集和触发更新的完整流程。但你可能注意到,这套机制是基于 Proxy 的——而 Proxy 只能代理对象,无法代理原始值。那么,如何让一个数字、一个字符串也具有响应式能力呢?这就是 ref 要解决的问题。
4.1 ref存在的意义
JavaScript的原始值(string、number、boolean等)是按值传递的,无法被Proxy代理:
let count = 0
const proxy = new Proxy(count, {}) // ❌ TypeError: Cannot create proxy with a non-object as target
即使能代理,原始值的赋值也无法被拦截:
let count = 0
count = 1 // 这是一个赋值操作,不是对象属性的修改,无法拦截
Vue的解决方案是ref——用一个对象包装原始值,通过getter/setter拦截对.value的访问:
// ref的本质:用对象包装原始值
const count = ref(0)
// 访问时通过.value,触发getter -> 收集依赖
console.log(count.value)
// 修改时通过.value,触发setter -> 触发更新
count.value++
这个设计虽然需要额外的.value访问,但换来了完整的响应式能力。而且在模板中,Vue会自动解包ref,所以实际使用时并不会感到繁琐。
4.2 RefImpl的实现
RefImpl是ref的核心实现类。与reactive不同,它不使用Proxy,而是直接通过class的getter/setter实现拦截:
// 文件: packages/reactivity/src/ref.ts
class RefImpl<T = any> {
_value: T
private _rawValue: T
dep: Dep = new Dep() // 每个ref有自己的Dep
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
this.dep.track() // 收集依赖
return this._value
}
set value(newValue) {
const oldValue = this._rawValue
// 判断是否需要直接使用值(shallow/readonly情况)
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||
isReadonly(newValue)
newValue = useDirectValue ? newValue : toRaw(newValue)
// 只有值真正变化时才触发更新
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
this.dep.trigger() // 触发更新
}
}
}
几个关键点:
1. 内置Dep:每个ref实例都有自己的Dep,不需要通过targetMap查找。这比reactive更直接,性能也更好。
2. 双重存储:_rawValue存储原始值(用于比较),_value存储可能被转换后的值。
3. 对象值自动转reactive:当ref包装的是对象时,会调用toReactive将其转为响应式:
const objRef = ref({ count: 0 })
// objRef.value 实际上是一个reactive对象
objRef.value.count++ // ✅ 这也是响应式的!
4. hasChanged检查:使用Object.is比较新旧值,避免不必要的更新。特别处理了NaN的情况(NaN !== NaN但Object.is(NaN, NaN)为true)。
4.3 ref家族的成员
Vue提供了多种ref变体,适用于不同场景:
classDiagram
class Ref {
<<interface>>
+get value()
+set value()
}
class RefImpl {
-_value: T
-_rawValue: T
+dep: Dep
"标准ref"
}
class CustomRefImpl {
+dep: Dep
-_get: Function
-_set: Function
"自定义track/trigger"
}
class ObjectRefImpl {
-_object: T
-_key: K
"委托给源对象"
}
class GetterRefImpl {
-_getter: Function
"只读getter ref"
}
Ref <|.. RefImpl
Ref <|.. CustomRefImpl
Ref <|.. ObjectRefImpl
Ref <|.. GetterRefImpl
shallowRef
shallowRef不会对对象值调用toReactive,只有.value本身的变化才会触发更新:
const state = shallowRef({ count: 0 })
state.value.count++ // ❌ 不会触发更新
state.value = { count: 1 } // ✅ 触发更新
适用场景:包装大型对象或第三方库返回的数据,避免深度响应式的开销。
customRef
customRef允许你完全控制track和trigger的时机,非常适合实现防抖、节流等场景:
// 文件: packages/reactivity/src/ref.ts
class CustomRefImpl<T> {
public dep: Dep
private readonly _get: () => T
private readonly _set: (value: T) => void
constructor(factory: CustomRefFactory<T>) {
const dep = (this.dep = new Dep())
// 将track和trigger方法传给工厂函数
const { get, set } = factory(
dep.track.bind(dep),
dep.trigger.bind(dep)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
实现一个防抖ref:
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => ({
get() {
track() // 收集依赖
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 延迟触发更新
}, delay)
}
}))
}
const text = useDebouncedRef('hello')
text.value = 'world' // 200ms后才触发更新
toRef与ObjectRefImpl
toRef可以为响应式对象的属性创建一个ref,这个ref与源属性保持同步:
const state = reactive({ foo: 1 })
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
ObjectRefImpl的实现很巧妙——它不创建新的Dep,而是委托给源对象:
// 文件: packages/reactivity/src/ref.ts
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly [ReactiveFlags.IS_REF] = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K],
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue! : val
}
set value(newVal) {
this._object[this._key] = newVal
}
// 委托给源对象的Dep
get dep(): Dep | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
当访问fooRef.value时,实际上是访问state.foo,依赖收集发生在源对象上。
GetterRefImpl
当toRef接收一个函数时,会创建一个只读的getter ref:
// 文件: packages/reactivity/src/ref.ts
class GetterRefImpl<T> {
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_READONLY] = true
constructor(private readonly _getter: () => T) {}
get value() {
return this._getter() // 每次访问都执行getter
}
}
const props = defineProps(['foo'])
const fooRef = toRef(() => props.foo) // 只读,每次访问执行getter
triggerRef:手动触发更新
当使用shallowRef时,深层属性的修改不会自动触发更新。triggerRef提供了手动触发的能力:
// 文件: packages/reactivity/src/ref.ts
export function triggerRef(ref: Ref): void {
if ((ref as unknown as RefImpl).dep) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
}
}
使用场景:
const shallow = shallowRef({
greet: 'Hello, world'
})
watchEffect(() => {
console.log(shallow.value.greet) // 首次执行打印 "Hello, world"
})
// 深层修改不会触发更新
shallow.value.greet = 'Hello, universe'
// 手动触发更新
triggerRef(shallow) // 打印 "Hello, universe"
这在处理大型对象或需要批量修改后统一触发更新的场景非常有用:
const data = shallowRef({ items: [], total: 0, loading: false })
// 批量修改,只触发一次更新
data.value.items = fetchedItems
data.value.total = fetchedItems.length
data.value.loading = false
triggerRef(data) // 统一触发
4.4 proxyRefs与模板解包
在Vue模板中,我们可以直接使用count而不是count.value:
<template>
<div>{{ count }}</div> <!-- 不需要.value -->
</template>
<script setup>
const count = ref(0)
</script>
这是如何实现的?答案是proxyRefs——Vue在处理setup返回值时会调用它:
// 文件: packages/reactivity/src/ref.ts
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => {
// 访问__v_raw返回原始对象
if (key === ReactiveFlags.RAW) return target
// 自动解包ref
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 如果旧值是ref且新值不是ref,更新ref的value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
return Reflect.set(target, key, value, receiver)
},
}
export function proxyRefs<T extends object>(objectWithRefs: T) {
return isReactive(objectWithRefs)
? objectWithRefs // 已经是reactive,直接返回
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
unref函数很简单:
export function unref<T>(ref: MaybeRef<T>): T {
return isRef(ref) ? ref.value : ref
}
toValue:unref的增强版
Vue 3.3引入了toValue函数,它是unref的增强版,额外支持getter函数:
// 文件: packages/reactivity/src/ref.ts
export function toValue<T>(source: MaybeRefOrGetter<T>): T {
return isFunction(source) ? source() : unref(source)
}
toValue可以统一处理三种输入:
import { toValue, ref } from 'vue'
// 1. 普通值 -> 直接返回
toValue(1) // 1
// 2. ref -> 返回.value
toValue(ref(1)) // 1
// 3. getter函数 -> 执行并返回结果
toValue(() => 1) // 1
这在编写可组合函数(composables)时特别有用,让API更加灵活:
// 不使用toValue:需要处理多种情况
function useFeature(maybeRef) {
const value = computed(() => {
if (isRef(maybeRef)) return maybeRef.value
if (typeof maybeRef === 'function') return maybeRef()
return maybeRef
})
}
// 使用toValue:代码更简洁
function useFeature(maybeRefOrGetter) {
const value = computed(() => toValue(maybeRefOrGetter))
}
// 使用示例
useFeature(1) // 普通值
useFeature(ref(1)) // ref
useFeature(() => props.count) // getter,响应式地追踪props
toValue与unref的对比:
| 特性 | unref | toValue |
|---|---|---|
| 处理普通值 | ✅ 直接返回 | ✅ 直接返回 |
| 处理ref | ✅ 返回.value | ✅ 返回.value |
| 处理getter | ❌ 返回函数本身 | ✅ 执行并返回结果 |
| 引入版本 | Vue 3.0 | Vue 3.3 |
这就是模板中ref自动解包的原理。注意这是"浅层"解包——只解包第一层的ref,嵌套的ref不会被解包。
4.8 ref vs reactive:如何选择
| 特性 | ref | reactive |
|---|---|---|
| 支持类型 | 任意值(包括原始值) | 仅对象 |
| 访问方式 | 需要.value | 直接访问 |
| 解构 | 保持响应式 | 失去响应式 |
| 替换整体 | ✅ ref.value = newObj | ❌ 会失去响应式 |
| 模板中 | 自动解包 | 直接使用 |
// ref:可以替换整体
const state = ref({ count: 0 })
state.value = { count: 1 } // ✅ 响应式
// reactive:不能替换整体
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // ❌ 原来的响应式丢失
// ref:解构保持响应式
const { value } = ref(0) // value是普通值,但这种用法少见
// reactive:解构失去响应式
const { count } = reactive({ count: 0 }) // count是普通值
const { count } = toRefs(reactive({ count: 0 })) // ✅ count是ref
Vue官方推荐:优先使用ref,因为它更灵活、更一致。
本章涉及源码:
packages/reactivity/src/ref.ts- RefImpl、CustomRefImpl、ObjectRefImpl、toRef等
ref 解决了原始值的响应式问题,但如果一个值需要根据其他响应式数据计算得出呢?每次访问都重新计算是否太浪费?下一章,我们将深入 computed 的惰性求值与缓存策略。
🤔 深度思考
-
设计权衡:为什么 ref 需要
.value而不能像 reactive 那样直接访问?有没有可能设计一个不需要.value的 ref?(提示:考虑 JavaScript 的语言限制) -
内存开销:每个 ref 都有自己的 Dep 实例,而 reactive 的属性共享 targetMap。在什么场景下 ref 的内存开销会成为问题?如何优化?
-
toRef vs toRefs:
toRef创建的是 ObjectRefImpl,它委托给源对象的 Dep。如果源对象不是响应式的,toRef 还能工作吗?为什么? -
customRef 应用:customRef 可以用来实现防抖 ref,请思考如何用它实现一个"节流 ref"?与防抖有什么区别?