别只会用 ref,试试 customRef,打开响应式新世界

142 阅读2分钟

customRef 是 Vue 3 中提供的一个 响应式 API,允许你自定义 ref 的行为 —— 它是响应式系统的底层构建砖块之一。如果你已经玩转了 refreactive,但觉得有时候想插个拦截器、做点节流防抖之类的骚操作,那么 customRef 就是你打开新世界的钥匙。


🌱 基础介绍

customRef 允许我们手动控制 getter 和 setter 的行为,并且通过 tracktrigger 精确控制依赖收集与更新触发的时机。

  • track():在 get() 中调用,用于收集依赖, 告诉 Vue “我这里有人要监听我”, 如果不调用 track(),这个值更新时,Vue 不会重新渲染组件watch() 也不会触发
  • trigger():在 set() 中调用,用于触发依赖, 告诉 Vue “我变了,请通知所有监听我的人”

语法

customRef<T>(factory: (track: () => void, trigger: () => void) => {
  get: () => T
  set: (value: T) => void
}): Ref<T>

🔧 场景一:防抖输入(最经典用法)

import { customRef } from 'vue'

function useDebouncedRef(value, delay = 300) {
  let timeout
  return customRef((track, trigger) => ({
    get() {
      track()
      return value
    },
    set(newValue) {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        value = newValue
        trigger()
      }, delay)
    }
  }))
}

使用方式:

const keyword = useDebouncedRef('', 500)

watch(keyword, val => {
  console.log('debounced:', val)
})

🎯 说明:只有在用户停止输入 500ms 后,watch 才会触发。


🔍 场景二:只读 ref(拦截 set)

function useReadonlyRef(value) {
  return customRef((track) => ({
    get() {
      track()
      return value
    },
    set() {
      console.warn('This ref is readonly.')
    }
  }))
}

const readOnlyValue = useReadonlyRef('Vue3')

readOnlyValue.value = 'Try to change me'  // 控制台输出警告

🕵️ 场景三:带缓存机制的 ref(懒加载)

function useLazyRef(factory) {
  let loaded = false
  let val
  return customRef((track, trigger) => ({
    get() {
      track()
      if (!loaded) {
        val = factory()
        loaded = true
        trigger()
      }
      return val
    },
    set(newVal) {
      val = newVal
      trigger()
    }
  }))
}

const data = useLazyRef(() => {
  console.log('init only once')
  return Math.random()
})

✅ 初次访问才会触发 factory,后面就是缓存值。


🎛️ 场景四:双向绑定 + 中间状态控制(输入同步但延迟提交)

function useDelayedSubmitRef(initValue, delay = 1000) {
  let timeout
  let internalValue = initValue
  return customRef((track, trigger) => ({
    get() {
      track()
      return internalValue
    },
    set(value) {
      internalValue = value
      trigger() // 立即更新 UI

      clearTimeout(timeout)
      timeout = setTimeout(() => {
        // 模拟提交逻辑
        console.log('Submitted:', value)
      }, delay)
    }
  }))
}

🧠 总结一句话

customRef 就像是 Vue 响应式系统里开了个“钩子口”,你可以在 getset 之间 做任何你想做的事 —— 防抖、节流、只读、懒加载、数据拦截、中间态……只要你敢想,它就敢干


📦 建议用法

  • 💬 用户输入防抖(推荐)
  • 📦 表单缓存
  • 🧲 数据懒加载
  • 🔒 只读 ref 封装
  • 🧩 封装业务型组件时对外暴露特殊响应式值