Vue3中customRef详解
- [1. 核心概念与工作原理](#1. 核心概念与工作原理)
- [2. 为什么需要 `customRef`?](#2. 为什么需要
customRef?) - [3. 常见应用场景与代码示例](#3. 常见应用场景与代码示例)
-
- [场景一:实现防抖 Ref(最经典用例)](#场景一:实现防抖 Ref(最经典用例))
- [场景二:实现带验证的 Ref](#场景二:实现带验证的 Ref)
- [场景三:LocalStorage 同步 Ref](#场景三:LocalStorage 同步 Ref)
- [场景四:异步 Ref](#场景四:异步 Ref)
- [4. `customRef` 与普通 `ref` 的对比](#4.
customRef与普通ref的对比) - [5. 注意事项与最佳实践](#5. 注意事项与最佳实践)
customRef 是 Vue 3 提供的一个高级响应式 API,它允许开发者创建一个自定义的 ref 对象,并完全控制其依赖追踪(Dependency Tracking)和触发更新(Triggering Updates)的行为。
相较于普通的 ref,customRef 提供了更细粒度的控制权,适用于防抖、节流、异步数据获取、本地存储同步等复杂场景。
1. 核心概念与工作原理
基本结构
customRef 接收一个工厂函数作为参数。该工厂函数接收两个关键参数:track 和 trigger,并必须返回一个包含 get 和 set 方法的对象。
javascript
import { customRef } from 'vue'
const myCustomRef = customRef((track, trigger) => {
let value = 'initial value' // 内部状态,通常通过闭包保存
return {
get() {
// 1. 告诉 Vue 当前副作用(如组件渲染)依赖了这个值
track()
return value
},
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 最常见的使用场景,用于优化搜索框等高频输入场景。
javascript
import { customRef } from 'vue'
/**
* 创建一个防抖的 ref
* @param {*} initialValue 初始值
* @param {number} delay 延迟时间(毫秒)
*/
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
在设置值之前进行校验,如果值不合法,可以拒绝更新或记录错误信息。
javascript
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 // 假设 validator 返回错误消息字符串
// 注意:这里可以选择是否 trigger()。
// 如果希望视图显示错误信息,需要 trigger()
// 如果希望非法值不更新视图中的 value,则只更新 error 并 trigger
}
trigger()
}
}
})
}
// 使用示例
// const ageRef = useValidatedRef(18, (val) => {
// if (val >= 0 && val <= 120) return true
// return '年龄必须在 0-120 之间'
// })
场景三:LocalStorage 同步 Ref
实现响应式数据与浏览器本地存储的双向同步。
javascript
import { customRef } from 'vue'
function useLocalStorageRef(key, initialValue) {
// 从 localStorage 读取初始值,如果没有则使用默认值
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
localStorage.setItem(key, JSON.stringify(newValue))
trigger()
}
}
})
}
// 使用示例
// const theme = useLocalStorageRef('theme', 'light')
// 修改 theme.value 会自动更新 localStorage
场景四:异步 Ref
处理异步数据获取,例如在设置值时触发 API 请求。
javascript
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() // 触发最终结果更新
}
}
}
})
}
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),以便在项目中复用。