Vue3的customRef通过手动控制track与trigger,可封装防抖、自动埋点、本地存储等通用逻辑,实现数据驱动而非事件驱动,同时保持v-model双向绑定,使模板更干净、逻辑内聚且复用性强,适用于复杂响应式场景。
Vue3 customRef实战:防抖、本地存储和自动埋点
时间:2026-06-25
浏览:346
文章目录
日常开发中编写搜索框防抖功能,几乎是每位前端工程师的必修课。过去实现防抖通常只有两种方式:绑定原生 input 事件,或者单独封装一个防抖函数。

先回顾一下传统写法,问题一目了然。
复制代码<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 相当于为响应式变量增加了一个中控开关,提供两个核心控制能力:
track():用于收集依赖。当代码或模板读取该变量时调用,告知 Vue 当前有地方在使用它,后续变量更新时需通知这些依赖。如果不调用 track,变量变化后页面不会刷新。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>
容易踩的坑
- 定时器变量必须定义在 customRef 外层,不能写在 get 或 set 内部,否则每次赋值都会新建定时器,防抖会失效。
- 如果需要立即执行节流,当前方案不适用,防抖是停止操作后延迟执行,两者机制不同。
自动埋点 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:完全自定义读写逻辑,读和写均可拦截,适合统一封装通用功能(防抖、缓存、埋点、权限校验等)。
使用优势
- 逻辑内聚:所有延迟、缓存、埋点逻辑全部封装在 hook 内部,模板保持零污染。
- 复用性强:项目中多个搜索框、配置项,只需直接导入 hook 调用即可。
- 双向绑定不变:完美兼容 v-model,无需手动拆分 value 和事件绑定。
- 数据驱动:将控制逻辑放在数据层而非视图事件层,符合 Vue 的数据驱动设计思想。
适用场景汇总
- 输入框防抖搜索(useDebouncedRef)
- 表单配置自动操作埋点(useTrackedRef)
- 数据本地持久化缓存(useLocalStorageRef)
- 滚动、拖拽节流控制(useThrottleRef)
- 赋值前执行权限校验、数据格式化
- 读取变量时自动加载配套字典数据
AI 快速生成钩子的通用 Prompt
复制代码当前项目基于 Vue3 + TypeScript,采用
2026 年的开发工作中,完全不必死记 customRef 的写法,直接交给 AI 处理,3 秒即可生成可直接上线的代码,大幅减少重复编码的工作量。
普通前端与资深前端真正的差距,不在于掌握多少花哨的插件,而在于能否理解框架底层的运行机制。
Vue3 特意将 track 和 trigger 这两个底层方法开放出来,正是为了允许开发者在响应式的根源层面直接控制性能、拦截各类业务逻辑。无需再编写大量杂乱的拼接代码,转而采用工程化的思路统一管理数据流转,最终产出的代码会更加干净且易于维护。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。