Vue3中customRef详解

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)的行为。

相较于普通的 refcustomRef 提供了更细粒度的控制权,适用于防抖、节流、异步数据获取、本地存储同步等复杂场景。

1. 核心概念与工作原理

基本结构

customRef 接收一个工厂函数作为参数。该工厂函数接收两个关键参数:tracktrigger,并必须返回一个包含 getset 方法的对象。

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

普通的 refreactive 在赋值后会立即触发视图更新。但在某些业务场景下,我们需要干预这个过程:

  1. 防抖(Debounce):用户输入搜索关键词时,不希望每输入一个字符就发起一次请求,而是希望停止输入一段时间后再接口请求。
  2. 节流(Throttle):限制函数调用的频率。
  3. 异步处理:在设置值时执行异步操作(如 API 请求),并在完成后才更新视图。
  4. 数据验证:在设置值之前进行校验,只有合法的值才更新内部状态。
  5. 外部状态同步:将响应式数据与 localStoragesessionStorage 或 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. 注意事项与最佳实践

  1. 必须调用 tracktrigger

    • 如果在 get 中忘记调用 track(),Vue 无法知道组件依赖了这个值,导致值变化时视图不更新。
    • 如果在 set 中忘记调用 trigger(),修改值后视图不会重新渲染。
  2. 避免无限循环:

    • 不要在 getset 中直接访问其他响应式对象的 .value 并立即触发更新,这可能导致死循环。
    • 如果需要在 set 中更新其他 ref,请确保逻辑清晰,避免相互触发。
  3. 闭包保存状态:

    • customRef 内部的 value 变量通常通过闭包保存。不要直接暴露这个变量,而是通过 get/set 访问,以确保响应式系统的正常工作。
  4. 性能考虑:

    • customRef 提供了强大的控制力,但也带来了额外的代码复杂度。只有在普通 refcomputedwatch 无法满足需求时,才建议使用 customRef
  5. v-model 兼容:

    • customRef 返回的对象与普通 ref 一样,可以直接用于 v-model 绑定,这使得它在表单处理中非常有用(如防抖输入框)。
  6. 总结

customRef 是 Vue 3 响应式系统中的一个强大工具,它打破了默认响应式行为的限制,让开发者能够精确控制数据的读取和更新时机。通过合理使用 tracktrigger,可以实现防抖、节流、异步处理、数据验证等高级功能,从而提升应用的性能和用户体验。在实际开发中,建议将常用的 customRef 逻辑封装成 Composable 函数(Hooks),以便在项目中复用。

相关推荐
濮水大叔1 小时前
Hooks 之后,为什么前端仍需要“包裹”能力?
vue.js·react.js·前端框架
sugar__salt1 小时前
Document 切割:RAG 知识库数据预处理实战
javascript·langchain·embedding·js·rag·cheerio
小粉粉hhh1 小时前
React(三)——Redux
前端·javascript·react.js
山东点狮信息科技有限公司2 小时前
企业级开源OA系统推荐
vue.js·spring boot·性能优化·系统架构·开源
易知微EasyV数据可视化2 小时前
定制化3D组件开发:打造差异化孪生体验
前端·人工智能·经验分享·数字孪生
多加点辣也没关系2 小时前
JavaScript|第24章:事件循环与并发模型
开发语言·javascript·ecmascript
前端炒粉3 小时前
Vue2 SSE 流式对话完整前端代码
前端·sse·流式输出
BIM云平台开发3 小时前
【App.vue里跟踪页面跳转和用户ID】
开发语言·前端·javascript
汪汪大队u3 小时前
Zabbix 6.0 部署踩坑记:从启动失败到 Web 成功访问
前端·zabbix