← 返回文章列表
Vue·430 次阅读

customRef 实战:手写一个防抖输入框

搜索框防抖这个需求,我这些年至少写过五种写法:watch + setTimeout、lodash debounce 包一层、VueUse 的 refDebounced……直到用了一次 customRef,才找到最"Vue 味"的答案。

先看问题

普通 ref 配合 v-model 时,输入即更新,watch 立刻触发:

<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')

watch(keyword, (val) => {
  // 用户每敲一个字就发一次请求,后端哭了
  searchApi(val)
})
</script>

<template>
  <input v-model="keyword" placeholder="搜索文章" />
</template>

常见解法是在 watch 回调里加防抖。但这样"防抖"的逻辑散落在每个使用处,换个组件还得复制一遍。

customRef:把控制权拿回来

customRef 接收一个工厂函数,要求返回带 get / set 的对象,并提供两个工具:

  • track():在 get 里调用,告诉 Vue"有人读我了,记一下依赖"
  • trigger():你决定什么时候调用,告诉 Vue"值变了,通知更新"

也就是说,什么时候算"变了"完全由你说了算。

实现 useDebouncedRef

// composables/useDebouncedRef.js
import { customRef } from 'vue'

export function useDebouncedRef(initialValue, delay = 300) {
  let value = initialValue
  let timer = null

  return customRef((track, trigger) => {
    return {
      get() {
        track() // 收集依赖
        return value
      },
      set(newValue) {
        value = newValue // 立即存下最新值
        clearTimeout(timer)
        timer = setTimeout(() => {
          trigger() // 停顿 delay 毫秒后才通知更新
        }, delay)
      },
    }
  })
}

使用方干净得不像话:

<script setup>
import { watch } from 'vue'
import { useDebouncedRef } from '@/composables/useDebouncedRef'

const keyword = useDebouncedRef('', 300)

watch(keyword, (val) => {
  // 用户停手 300ms 后才会走到这里
  searchApi(val)
})
</script>

<template>
  <input v-model="keyword" placeholder="搜索文章" />
</template>

v-model 照常工作,watch 照常工作,但更新节奏被 ref 内部控制了。防抖逻辑从"每个使用处"收敛到了"ref 本身",这就是 customRef 的威力。

一个关键细节:为什么 set 里立即存值

注意上面 set 里 value = newValue 是立刻执行的,只有 trigger() 被延迟。如果反过来延迟赋值:

set(newValue) {
  clearTimeout(timer)
  timer = setTimeout(() => {
    value = newValue // ❌ 错误示范
    trigger()
  }, delay)
}

会有个诡异 bug:输入框显示的是 v-model 绑定的值,而 get 拿到的还是旧值,输入会"回跳"。输入框这种受控场景,内部值必须同步更新,只延迟通知。

进阶:带 loading 状态的请求 ref

customRef 还能玩更大的。封装一个"赋值即请求"的 ref:

export function useFetchRef(fetcher, initialValue = null) {
  const loading = ref(false)
  const error = ref(null)

  const data = customRef((track, trigger) => {
    let value = initialValue
    return {
      get() {
        track()
        return value
      },
      async set(params) {
        loading.value = true
        error.value = null
        try {
          value = await fetcher(params)
        } catch (e) {
          error.value = e
        } finally {
          loading.value = false
          trigger()
        }
      },
    }
  })

  return { data, loading, error }
}
const { data: user, loading } = useFetchRef(fetchUser)

user.value = { id: 42 } // 赋值触发请求,模板里 {{ loading }} 自动转圈

这个写法见仁见智(语义上有点"赋值即副作用"的激进),但在内部工具库里用得很顺手。

customRef vs ref + watch 防抖

方案 防抖逻辑位置 v-model 兼容 复用成本
watch + debounce 每个使用处 天然 复制粘贴
VueUse refDebounced 独立新 ref 要包一层 computed 低
customRef ref 内部 天然 低

refDebounced 也很好用,但它产生一个新 ref,和输入框之间还得再传一道。customRef 方案是单一 ref 贯穿始终,我个人更偏爱。

小结

  • customRef 把"何时算变化"的控制权交给开发者:get 里 track、自己决定 trigger 时机
  • 防抖 ref 的关键:set 里立即存值、延迟 trigger,否则输入回跳
  • 和 v-model、watch 完全兼容,防抖逻辑收敛到 ref 内部
  • 理解了 customRef,也就真正理解了 Vue 响应式的依赖收集与触发机制

Vue 3 响应式系统暴露给我们的 API 不多,但每一个(ref、computed、watchEffect、customRef)都刚好打在一个关键控制点上。值得花时间一个个吃透。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4