Vue3中customRef详解
Vue3中customRef详解
- 1. 核心概念与工作原理
- 2. 为什么需要 `customRef`?
- 3. 常见应用场景与代码示例
- 场景一:实现防抖 Ref(最经典用例)
- 场景二:实现带验证的 Ref
- 场景三:LocalStorage 同步 Ref
- 场景四:异步 Ref
- 4. `customRef` 与普通 `ref` 的对比
- 5. 注意事项与最佳实践
customRef是 Vue 3 提供的一个高级响应式 API,它允许开发者创建一个自定义的ref对象,并完全控制其依赖追踪(Dependency Tracking)和触发更新(Triggering Updates)的行为。
相较于普通的ref,customRef提供了更细粒度的控制权,适用于防抖、节流、异步数据获取、本地存储同步等复杂场景。
1. 核心概念与工作原理
基本结构customRef接收一个工厂函数作为参数。该工厂函数接收两个关键参数:track和trigger,并必须返回一个包含get和set方法的对象。
import{customRef}from'vue'constmyCustomRef=customRef((track,trigger)=>{letvalue='initial value'// 内部状态,通常通过闭包保存return{get(){// 1. 告诉 Vue 当前副作用(如组件渲染)依赖了这个值track()returnvalue},set(newValue){// 2. 更新内部值value=newValue// 3. 告诉 Vue 值已改变,需要重新运行依赖该值的副作用trigger()}}})关键函数说明
track():在get方法中调用。用于通知 Vue 的响应式系统:“当前正在读取这个值,请收集依赖”。如果不调用track(),即使值改变了,视图也不会更新。trigger():在set方法中调用。用于通知 Vue 的响应式系统:“这个值已经改变,请通知所有依赖它的地方进行更新”。如果不调用trigger(),修改值后视图不会重新渲染。
2. 为什么需要customRef?
普通的ref或reactive在赋值后会立即触发视图更新。但在某些业务场景下,我们需要干预这个过程:
- 防抖(Debounce):用户输入搜索关键词时,不希望每输入一个字符就发起一次请求,而是希望停止输入一段时间后再接口请求。
- 节流(Throttle):限制函数调用的频率。
- 异步处理:在设置值时执行异步操作(如 API 请求),并在完成后才更新视图。
- 数据验证:在设置值之前进行校验,只有合法的值才更新内部状态。
- 外部状态同步:将响应式数据与
localStorage、sessionStorage或 Web Worker 保持同步。
3. 常见应用场景与代码示例
场景一:实现防抖 Ref(最经典用例)
这是customRef最常见的使用场景,用于优化搜索框等高频输入场景。
import{customRef}from'vue'/** * 创建一个防抖的 ref * @param {*} initialValue 初始值 * @param {number} delay 延迟时间(毫秒) */functionuseDebouncedRef(initialValue,delay=500){lettimeoutIdletvalue=initialValuereturncustomRef((track,trigger)=>{return{get(){track()// 收集依赖returnvalue},set(newValue){// 每次设置新值时,清除之前的定时器clearTimeout(timeoutId)// 设置新的定时器,延迟更新值和触发视图更新timeoutId=setTimeout(()=>{value=newValuetrigger()// 触发更新},delay)}}})}// 在组件中使用// const searchText = useDebouncedRef('', 500)// <input v-model="searchText" />// 视图只有在停止输入 500ms 后才会更新场景二:实现带验证的 Ref
在设置值之前进行校验,如果值不合法,可以拒绝更新或记录错误信息。
import{customRef}from'vue'functionuseValidatedRef(initialValue,validator){letvalue=initialValueleterror=nullreturncustomRef((track,trigger)=>{return{get(){track()return{value,error}// 返回包含值和错误信息的对象},set(newValue){constisValid=validator(newValue)if(isValid===true){value=newValue error=null}else{error=isValid// 假设 validator 返回错误消息字符串// 注意:这里可以选择是否 trigger()。// 如果希望视图显示错误信息,需要 trigger()// 如果希望非法值不更新视图中的 value,则只更新 error 并 trigger}trigger()}}})}// 使用示例// const ageRef = useValidatedRef(18, (val) => {// if (val >= 0 && val <= 120) return true// return '年龄必须在 0-120 之间'// })场景三:LocalStorage 同步 Ref
实现响应式数据与浏览器本地存储的双向同步。
import{customRef}from'vue'functionuseLocalStorageRef(key,initialValue){// 从 localStorage 读取初始值,如果没有则使用默认值conststoredValue=localStorage.getItem(key)letvalue=storedValue?JSON.parse(storedValue):initialValuereturncustomRef((track,trigger)=>{return{get(){track()returnvalue},set(newValue){value=newValue// 同步写入 localStoragelocalStorage.setItem(key,JSON.stringify(newValue))trigger()}}})}// 使用示例// const theme = useLocalStorageRef('theme', 'light')// 修改 theme.value 会自动更新 localStorage场景四:异步 Ref
处理异步数据获取,例如在设置值时触发 API 请求。
import{customRef}from'vue'functionuseAsyncRef(asyncFetcher,initialValue=null){letvalue=initialValueletloading=falseleterror=nullreturncustomRef((track,trigger)=>{return{get(){track()return{value,loading,error}},asyncset(params){loading=trueerror=nulltrigger()// 触发加载状态更新try{constresult=awaitasyncFetcher(params)value=result}catch(e){error=e}finally{loading=falsetrigger()// 触发最终结果更新}}}})}4.customRef与普通ref的对比
| 特性 | ref() | customRef() |
|---|---|---|
| 依赖追踪 | 自动 | 手动调用track() |
| 触发更新 | 自动 | 手动调用trigger() |
| 灵活性 | 低,仅基本响应式 | 高,可插入任意逻辑 |
| 适用场景 | 大多数常规状态管理 | 防抖、节流、异步、验证、外部同步 |
| 代码复杂度 | 简单 | 较复杂,需维护内部状态 |
5. 注意事项与最佳实践
必须调用
track和trigger:- 如果在
get中忘记调用track(),Vue 无法知道组件依赖了这个值,导致值变化时视图不更新。 - 如果在
set中忘记调用trigger(),修改值后视图不会重新渲染。
- 如果在
避免无限循环:
- 不要在
get或set中直接访问其他响应式对象的.value并立即触发更新,这可能导致死循环。 - 如果需要在
set中更新其他ref,请确保逻辑清晰,避免相互触发。
- 不要在
闭包保存状态:
customRef内部的value变量通常通过闭包保存。不要直接暴露这个变量,而是通过get/set访问,以确保响应式系统的正常工作。
性能考虑:
customRef提供了强大的控制力,但也带来了额外的代码复杂度。只有在普通ref、computed或watch无法满足需求时,才建议使用customRef。
与
v-model兼容:customRef返回的对象与普通ref一样,可以直接用于v-model绑定,这使得它在表单处理中非常有用(如防抖输入框)。
总结
customRef是 Vue 3 响应式系统中的一个强大工具,它打破了默认响应式行为的限制,让开发者能够精确控制数据的读取和更新时机。通过合理使用track和trigger,可以实现防抖、节流、异步处理、数据验证等高级功能,从而提升应用的性能和用户体验。在实际开发中,建议将常用的customRef逻辑封装成 Composable 函数(Hooks),以便在项目中复用。
