Skip to content

响应式工具:toRefs、customRef 等 API 的实现与应用场景

Vue 提供了 refreactive 作为基础的响应式 API。此外,它还提供了一系列工具函数,用于处理特定场景下的响应式数据转换和控制。本节将分析 toRefscustomRef 等核心工具的实现与应用。

toRefstoRef:解决 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)
}

ObjectRefImpltoRef 的核心实现。它本身不存储值,而是作为原始对象的“属性引用”:

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,从而自动触发了 tracktrigger,保持了响应式连接。


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 的核心是让用户接管 tracktrigger 的调用时机。

应用场景:实现防抖 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 在编写组合式函数时非常有用,它允许函数参数灵活地接收 refgetter 或普通值。

总结

Vue 的响应式工具 API 为处理常见的响应式问题提供了标准方案:

  • toRefs / toRef:通过 ObjectRefImpl 委托属性的 get/set,解决了 reactive 对象解构时丢失响应性的问题。
  • customRef:提供 tracktrigger 函数,允许用户自定义依赖收集和派发更新的时机。
  • triggerRef:用于手动触发 shallowRef 的更新,当其内部值改变但引用未变时使用。
  • proxyRefs:通过 Proxy 拦截 getset,实现 ref 值的自动解包和设置,简化了模板中的使用。
  • unref / toValue:提供了统一访问 refgetter 或普通值的方法。

微信公众号二维码

Last updated: