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 条
还没有评论,来抢沙发~