游乐游手机版
首页/前端开发/文章详情

Vue3 customRef教程:防抖、本地存储、自动埋点一套搞定

时间:2026-07-28 06:16
Vue3的customRef通过手动控制track与trigger,可封装防抖、自动埋点、本地存储等通用逻辑,实现数据驱动而非事件驱动,同时保持v-model双向绑定,使模板更干净、逻辑内聚且复用性强,适用于复杂响应式场景。

日常开发中,实现搜索框防抖几乎是每位前端工程师的必修课。过去我们通常只有两种选择:要么绑定原生 input 事件,要么额外封装一个防抖函数。

Vue3 customRef 封神教程:防抖、本地存储、自动埋点一套搞定,模板干干净净

先来看看传统写法有多繁琐——一眼就能看出问题所在。

 复制代码<script setup>
import { ref } from 'vue';
import { debounce } from 'lodash-es';
    
const keyword = ref('');
// 仅仅为了实现防抖,就得额外编写一个独立函数
const handleInput = debounce((e) => {
  keyword.value = e.target.value;
  fetchData(keyword.value);
}, 500);
script>
​
<template>
  
  <input :value="keyword" @input="handleInput" placeholder="请输入物料编码..." />
template>

这段代码虽然能正常运行,但怎么看都觉得别扭。Vue 的核心优势在于 v-model 实现的数据双向绑定,可一旦引入防抖,数据流就被硬生生拆散了,模板与业务逻辑纠缠不清,页面一多维护成本直线上升。

那么,有没有更优雅的解决方案呢?既能保留 v-model="keyword" 的简洁写法,无需手动绑定事件,又能让变量自带防抖效果?

答案是 Vue3 内置的 customRef,它完美地解决了这一痛点。

customRef 究竟是什么?

普通的 ref 堪称“一根筋”。只要你修改值,它就会立刻通知页面刷新,没有任何缓冲或拦截的余地。

而 customRef 相当于给响应式变量安装了一个中控开关,赋予你两个核心控制权:

  1. track():收集依赖。当代码或模板读取该变量时调用,告知 Vue:“现在有地方在使用我,后续我更新了需要通知它。” 如果不调用它,变量变了页面也不会刷新。
  2. trigger():触发更新。当你认为时机成熟时调用它,Vue 才会更新页面并触发 watch 监听。

简单来说:普通 ref 会自动执行 track 和 trigger,而 customRef 则把这两步交给你手动掌控——何时更新、是否延迟更新、修改前追加额外逻辑,全由你决定。

封装防抖 Ref useDebouncedRef

我们来封装一个通用钩子,用它创建的变量天生具备防抖延迟,可以直接搭配 v-model 使用。

封装代码 composables/useDebouncedRef.ts

 复制代码import { customRef } from 'vue';
​
// 泛型兼容所有数据类型,默认防抖500ms
export function useDebouncedRef(initialValue: T, delay = 500) {
  let timeout: number | null = null;
​
  // customRef 接收回调,入参为 track 和 trigger
  const refInstance = customRef((track, trigger) => {
    return {
      // 获取变量值时触发
      get() {
        track(); // 必须调用,用于收集依赖
        return initialValue;
      },
      // 给变量赋值时触发
      set(newValue: T) {
        // 每次输入先清除上一次定时器,实现防抖核心逻辑
        if (timeout) clearTimeout(timeout);
        timeout = window.setTimeout(() => {
          initialValue = newValue;
          trigger(); // 延迟结束后才通知Vue更新
      }, delay);
      }
    };
  });
    
  // 组件卸载时清除定时器,防止内存泄漏
  onUnmounted(() => {
    if (timeout) clearTimeout(timeout);
  });
  return refInstance;
}

页面使用示例

 复制代码<script setup lang="ts">
import { watch } from 'vue';
import { useDebouncedRef } from '@/composables/useDebouncedRef';
import { fetchLimsData } from '@/api';
​
// 生成自带500ms防抖的响应式变量
const keyword = useDebouncedRef('', 500);
​
// 只有停止输入500ms后,才会执行接口请求
watch(keyword, (newVal) => {
  fetchLimsData(newVal);
});
script>
​
<template>
  
  <a-input v-model:value="keyword" placeholder="扫码或输入批次号..." />
template>

容易踩坑的细节

  1. 定时器变量必须放在 customRef 外层,不能写在 get/set 内部,否则每次赋值都会新建定时器,导致防抖失效;
  2. 如果需要立即执行节流,这个方案不适用,防抖的本质是停止操作后延迟执行。

自动埋点 Ref useTrackedRef

业务场景:表单开关、高危配置修改,产品要求只要改动就要记录操作日志。如果挨个给每个控件绑定 @change 事件,实在太繁琐。

利用 customRef 拦截赋值操作,修改值的同时自动上报埋点,无需改动模板一行代码。

封装代码 composables/useTrackedRef.ts

 复制代码import { customRef } from 'vue';
import { reportLog } from '@/utils/monitor';
​
// actionName:用于区分当前是哪个配置项
export function useTrackedRef(initialValue: T, actionName: string) {
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return initialValue;
      },
      set(newValue: T) {
        // 只有新旧值不同才上报埋点,避免无意义的重复上报
        if (initialValue !== newValue) {
          reportLog(`用户修改配置【${actionName}】,原值:${initialValue},新值:${newValue}`);
          initialValue = newValue;
          trigger();
        }
      }
    };
  });
}

页面使用

 复制代码<script setup lang="ts">
import { useTrackedRef } from '@/composables/useTrackedRef';
// 设备自动启动开关,修改后自动埋点
const isAutoStart = useTrackedRef(false, '自动启动设备开关');
// 产线高危重启配置
const forceReboot = useTrackedRef(false, '产线强制重启开关');
script>
​
<template>
  <a-switch v-model:checked="isAutoStart" />
  <a-switch v-model:checked="forceReboot" />
template>

只要切换开关,无需添加任何事件,后端就能自动收到操作审计日志。

持久化本地存储 Ref useLocalStorageRef

变量修改后自动存入 localStorage,页面刷新时优先读取缓存,同时支持对象与数组的序列化。

封装代码 composables/useLocalStorageRef.ts

 复制代码import { customRef } from 'vue';
​
export function useLocalStorageRef(key: string, defaultValue: T) {
  // 初始化时读取本地缓存
  let initVal: T;
  try {
    const cache = localStorage.getItem(key);
    initVal = cache ? JSON.parse(cache) : defaultValue;
  } catch (err) {
    // 解析失败或缓存损坏时,使用默认值
    console.warn('本地缓存解析失败', err);
    initVal = defaultValue;
  }
​
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return initVal;
      },
      set(newVal: T) {
        initVal = newVal;
        trigger();
        try {
          // 同步写入本地存储
          localStorage.setItem(key, JSON.stringify(newVal));
        } catch (err) {
          console.error('本地存储写入失败', err);
        }
      }
    };
  });
}

使用示例

 复制代码<script setup lang="ts">
// 保存用户筛选条件,刷新页面后不丢失
const searchFilter = useLocalStorageRef('material_search_filter', {
  code: '',
  status: 1
});
script>
​
<template>
  <a-input v-model:value="searchFilter.code" />
  <a-select v-model:value="searchFilter.status" />
template>

核心知识点补充

customRef 与普通 ref、shallowRef 的区别

  • ref:基础响应式,自动执行 track 和 trigger,无法拦截读写操作;
  • shallowRef:仅监听第一层数据,深层属性变更不会触发更新;
  • customRef:完全自定义读写逻辑,读写均可拦截,适合封装通用功能(防抖、缓存、埋点、权限校验等)。

使用优势

  1. 逻辑内聚:所有延迟、缓存、埋点逻辑全部封装在 hook 内部,模板零污染;
  2. 复用性强:全项目中的搜索框、配置项,直接导入对应 hook 即可使用;
  3. 双向绑定不变:完美兼容 v-model,无需手动拆解 value 和事件;
  4. 数据驱动:将控制逻辑放在数据层而非视图事件层,符合 Vue 数据驱动的核心理念。

适用场景汇总

  • 输入框防抖搜索(useDebouncedRef)
  • 表单配置自动操作埋点(useTrackedRef)
  • 数据本地持久化缓存(useLocalStorageRef)
  • 滚动、拖拽节流控制(useThrottleRef)
  • 赋值前进行权限校验、数据格式化
  • 读取变量时自动加载配套字典数据

AI 快速生成钩子的万能 Prompt

 复制代码当前项目基于 Vue3 + TypeScript,采用