Appearance
响应式工具:toRefs、customRef 等 API 的实现与应用场景
Vue 提供了 ref 和 reactive 作为基础的响应式 API。此外,它还提供了一系列工具函数,用于处理特定场景下的响应式数据转换和控制。本节将分析 toRefs、customRef 等核心工具的实现与应用。
toRefs 与 toRef:解决 reactive 解构丢失响应性的问题
在组合式 API 中,如果直接解构 reactive 对象,会使其丢失响应性。
javascript
const state = reactive({ count: 0 });
// 错误:count 只是一个普通的数字 0,不是响应式的
const { count } = state;
// 在 setup 中对 props 解构同理
setup(props) {
// 错误:props.name 变化时,name 不会更新
const { name } = props;
}解决方案:toRefs
toRefs 专为此场景设计。它遍历 reactive 对象,为它的每一个属性都创建一个 ref。
javascript
// 正确:
const state = reactive({ count: 0 });
const { count } = toRefs(state);
count.value++; // 这会正确地修改 state.count实现原理:ObjectRefImpl
toRefs 会遍历对象,并为每个属性调用 propertyToRef:
typescript
// core/packages/reactivity/src/ref.ts
export function toRefs<T extends object>(object: T): ToRefs<T> {
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
// 核心:为每个 key 创建一个“属性 ref”
ret[key] = propertyToRef(object, key)
}
return ret
}
function propertyToRef(source: Record<string, any>, key: string) {
// `toRef` 的核心是创建 ObjectRefImpl 实例
return new ObjectRefImpl(source, key)
}ObjectRefImpl 是 toRef 的核心实现。它本身不存储值,而是作为原始对象的“属性引用”:
typescript
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly [ReactiveFlags.IS_REF] = true
constructor(private readonly _object: T, private readonly _key: K) {}
// get value():
// 当读取 .value 时,
// 它的 get 操作被“委托”给原始 reactive 对象的属性读取
get value() {
return this._object[this._key]
}
// set value(newVal):
// 当写入 .value 时,
// 它的 set 操作被“委托”给原始 reactive 对象的属性设置
set value(newVal) {
this._object[this._key] = newVal
}
}toRefs 通过创建 ObjectRefImpl 实例,将 get/set 操作委托回原始 reactive 对象的 Proxy,从而自动触发了 track 和 trigger,保持了响应式连接。
customRef:自定义依赖收集与更新时机
有时,我们需要更精细地控制 ref 的响应式行为,例如实现“防抖 ref”。
解决方案:customRef
customRef 是一个工厂函数,它允许开发者自定义 ref 的依赖收集(track)和更新触发(trigger)逻辑。
typescript
// core/packages/reactivity/src/ref.ts
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
// CustomRefImpl 只是一个包装器
// 它调用 factory,并保存返回的 get 和 set
return new CustomRefImpl(factory) as any
}
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())
// 关键:将“依赖收集器”和“触发器”交给 factory 函数
const { get, set } = factory(
() => dep.track(), // 这是 track 函数
() => dep.trigger() // 这是 trigger 函数
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}customRef 的核心是让用户接管 track 和 trigger 的调用时机。
应用场景:实现防抖 ref
javascript
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
// 1. 调用 track(),注册依赖
track()
return value
},
set(newValue) {
// 2. set 时,不立即赋值和 trigger
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 3. 延迟 200ms 后,调用 trigger() 派发更新
trigger()
}, delay)
}
}
})
}
// 用法:
const searchQuery = useDebouncedRef('', 500)
watchEffect(() => {
// 这个 effect 只会在 500ms 停止输入后才执行
console.log(searchQuery.value)
})triggerRef:强制触发 shallowRef 的更新
shallowRef 只对 .value 的根引用进行响应式处理。如果你修改了 shallowRef 内部对象的属性,Vue 无法侦测到变化。
javascript
const state = shallowRef({ count: 0 })
// 错误:这不会触发任何更新
// 因为 state.value 的“引用”没有变
state.value.count = 1
// 正确:这会触发更新
state.value = { count: 1 }但有时我们确实需要修改内部值,并手动通知 Vue 更新。
解决方案:triggerRef
triggerRef 允许你手动触发一个 ref 的更新(trigger)流程。
typescript
// core/packages/reactivity/src/ref.ts
export function triggerRef(ref: Ref): void {
// 直接获取 ref 内部的 dep(订阅者列表),并手动执行 trigger()
if ((ref as unknown as RefImpl).dep) {
(ref as unknown as RefImpl).dep.trigger()
}
}应用场景:shallowRef 的深度修改
javascript
const state = shallowRef({ count: 0 })
watchEffect(() => {
console.log(state.value.count) // 依赖了 state.value
})
state.value.count = 1 // 不会触发 watchEffect
// 手动通知 Vue:state.value 变了,请更新!
triggerRef(state)
// "1" 被打印出来proxyRefs:模板中的自动解包
ref 必须通过 .value 访问,但在 setup() 返回 ref 后,我们在模板中却可以直接使用,无需 .value。
解决方案:proxyRefs
Vue 内部使用 proxyRefs 来“自动解包”。setup() 函数返回的对象会被 proxyRefs 包装。
typescript
// core/packages/reactivity/src/ref.ts
const shallowUnwrapHandlers: ProxyHandler<any> = {
// 核心在 get 拦截器
get: (target, key, receiver) => {
// 1. 正常读取
const value = Reflect.get(target, key, receiver)
// 2. 自动解包:如果读出的是 ref,则返回它的 .value
return isRef(value) ? value.value : value
},
// set 拦截器
set: (target, key, value, receiver) => {
const oldValue = target[key]
// 3. 自动写入 .value:
// 如果旧值是 ref,新值不是,则自动设置 .value
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
// 否则,正常替换
return Reflect.set(target, key, value, receiver)
}
}
}
export function proxyRefs<T extends object>(objectWithRefs: T) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers)
}proxyRefs 提供了模板中 .value 自动解包的机制。reactive 内部在设置属性时也会使用此逻辑,因此 reactive({ a: ref(0) }) 也能自动解包。
其他工具
1. 类型检查 (isRef, isReactive, isReadonly)
这些工具用于检查一个值是否是响应式对象。它们通过检查对象上是否存在特定的“内部标记”来实现:
typescript
export function isRef(r: any): r is Ref {
return !!(r && r[ReactiveFlags.IS_REF] === true)
}
export function isReactive(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}2. 值提取 (unref, toValue)
unref 是一个获取 ref 值的辅助函数,如果参数不是 ref,则原样返回。
typescript
// 相当于 val = isRef(val) ? val.value : val
export function unref<T>(ref: MaybeRef<T>): T {
return isRef(ref) ? ref.value : ref
}toValue (Vue 3.3+ 新增) 是 unref 的增强版,它不仅能解包 ref,还能执行 getter 函数:
typescript
export function toValue<T>(source: MaybeRefOrGetter<T>): T {
return isFunction(source) ? source() : unref(source)
}toValue 在编写组合式函数时非常有用,它允许函数参数灵活地接收 ref、getter 或普通值。
总结
Vue 的响应式工具 API 为处理常见的响应式问题提供了标准方案:
toRefs/toRef:通过ObjectRefImpl委托属性的get/set,解决了reactive对象解构时丢失响应性的问题。customRef:提供track和trigger函数,允许用户自定义依赖收集和派发更新的时机。triggerRef:用于手动触发shallowRef的更新,当其内部值改变但引用未变时使用。proxyRefs:通过Proxy拦截get和set,实现ref值的自动解包和设置,简化了模板中的使用。unref/toValue:提供了统一访问ref、getter或普通值的方法。
