customRef是Vue 3提供的高级响应式API,它允许开发者创建自定义ref对象,并完全控制依赖追踪和触发更新的时机。与普通ref相比,customRef在读取和赋值时可以插入自定义逻辑,因此适合防抖、验证、异步获取、本地存储同步等场景。
首先看customRef的基本结构。customRef接收一个工厂函数,工厂函数接收track和trigger两个参数,并返回包含get和set的对象。track在get中调用,用于收集依赖;trigger在set中调用,用于通知Vue值已变化。若get中缺少track,值变化时视图不会更新;若set中缺少trigger,赋值后不会触发重新渲染。
下面是一个最基础的自定义ref:- import { customRef } from 'vue'
- const myCustomRef = customRef((track, trigger) => {
- let value = 'initial value'
- return {
- get() {
- track()
- return value
- },
- set(newValue) {
- value = newValue
- trigger()
- }
- }
- })
复制代码
为什么需要customRef?因为普通ref赋值后立即触发视图更新,而业务中可能需要干预这个过程,比如输入防抖、节流、异步处理、数据验证、外部状态同步等。
场景一:防抖Ref。这是最经典用例,常用于搜索框。每次设置值时清除之前的定时器,延迟delay毫秒后再真正更新值和触发更新:- import { customRef } from 'vue'
- function useDebouncedRef(initialValue, delay = 500) {
- let timeoutId
- let value = initialValue
- return customRef((track, trigger) => {
- return {
- get() {
- track()
- return value
- },
- set(newValue) {
- clearTimeout(timeoutId)
- timeoutId = setTimeout(() => {
- value = newValue
- trigger()
- }, delay)
- }
- }
- })
- }
复制代码
组件里这样使用:const searchText = useDebouncedRef('', 500),再把 input 的 v-model 指向 searchText,停止输入500ms后才会更新视图。
场景二:带验证的Ref。在set中通过validator函数校验,合法才更新value,非法则把错误消息保存到error并触发更新:- import { customRef } from 'vue'
- function useValidatedRef(initialValue, validator) {
- let value = initialValue
- let error = null
- return customRef((track, trigger) => {
- return {
- get() {
- track()
- return { value, error }
- },
- set(newValue) {
- const isValid = validator(newValue)
- if (isValid === true) {
- value = newValue
- error = null
- } else {
- error = isValid
- }
- trigger()
- }
- }
- })
- }
复制代码
使用时传入校验函数,例如校验年龄在0-120之间,非法时返回错误消息字符串。
场景三:LocalStorage同步Ref。读取初始值时优先从localStorage获取,set时同步写入localStorage,实现响应式数据与本地存储双向绑定:- import { customRef } from 'vue'
- function useLocalStorageRef(key, initialValue) {
- const storedValue = localStorage.getItem(key)
- let value = storedValue ? JSON.parse(storedValue) : initialValue
- return customRef((track, trigger) => {
- return {
- get() {
- track()
- return value
- },
- set(newValue) {
- value = newValue
- localStorage.setItem(key, JSON.stringify(newValue))
- trigger()
- }
- }
- })
- }
复制代码
比如const theme = useLocalStorageRef('theme', 'light'),修改theme.value时会自动同步到localStorage。
场景四:异步Ref。可以在set中触发API请求,并用loading和error状态辅助展示,请求完成后更新值:- import { customRef } from 'vue'
- function useAsyncRef(asyncFetcher, initialValue = null) {
- let value = initialValue
- let loading = false
- let error = null
- return customRef((track, trigger) => {
- return {
- get() {
- track()
- return { value, loading, error }
- },
- async set(params) {
- loading = true
- error = null
- trigger()
- try {
- const result = await asyncFetcher(params)
- value = result
- } catch (e) {
- error = e
- } finally {
- loading = false
- trigger()
- }
- }
- }
- })
- }
复制代码
customRef与普通ref的对比:普通ref依赖追踪自动、触发更新自动、灵活性低,适用常规状态管理;customRef需要手动调用track和trigger,可在get/set中插入任意逻辑,适合防抖、节流、异步、验证、外部同步等,但代码复杂度更高。
使用customRef的注意事项:第一,get中必须调用track,set中必须调用trigger,否则依赖收集或更新通知会失效。第二,避免在get或set中直接访问其他响应式对象的.value并立即触发更新,防止循环依赖。第三,内部状态通常用闭包保存,不要直接暴露value变量。第四,customRef控制力强但复杂度也高,普通ref、computed或watch能满足需求时优先使用它们。第五,customRef返回的对象与普通ref一样可以直接用于v-model绑定,所以很适合做防抖输入框等表单场景。
总结:customRef打破了默认响应式行为的限制,开发者通过track和trigger精确控制数据读取与更新时机,可以实现防抖、节流、异步处理、数据验证等功能,提升应用性能与用户体验。实际开发中建议将常用逻辑封装成Composable函数(Hooks),便于复用。 |