在 Vue 3 的响应式系统中,customRef 是一个功能强大但常被忽视的 API。它允许开发者完全自定义一个 ref 的响应式行为,直接暴露底层的 track 和 trigger 机制。这种低级别的控制能力,使得实现复杂的响应式逻辑成为可能。本节将通过手写一个“防抖 ref”的实例,深入剖析 customRef 的设计原理,并揭示其在构建高级响应式工具中的核心价值。
一、customRef 的 API 设计
customRef 接收一个工厂函数作为参数,该函数返回一个包含 get 和 set 方法的对象:
function customRef(factory) {
const ref = factory(
() => track(ref, 'value'), // track 函数
() => trigger(ref, 'value') // trigger 函数
);
ref.__v_isRef = true;
return ref;
}factory 函数接收两个参数:track 和 trigger。开发者可以在 get 中调用 track 来声明依赖,在 set 中调用 trigger 来通知依赖更新。customRef 不提供自动的响应式追踪,一切行为由你定义。
这与 ref 形成鲜明对比:
ref:自动在 getter 中track,在 setter 中trigger。customRef:完全手动控制何时track和trigger。
二、需求分析:为什么需要防抖 ref?
在用户交互场景中,频繁的状态更新(如输入框输入、鼠标移动)会触发大量视图更新,可能导致性能问题。
const input = ref('');
watch(input, () => {
// 每次输入都触发,可能调用 API
});传统的防抖方案通常在事件处理函数中实现:
const debouncedSearch = debounce(() => {
// 执行搜索
}, 300);
// 在 input 事件中调用 debouncedSearch但这种方式将防抖逻辑与事件处理耦合,且无法在 watch 或计算属性中直接使用。
一个“防抖 ref”可以将防抖逻辑内置于响应式系统中,使得任何对它的读写都自动具备防抖特性,实现关注点分离。
三、实现:手写 debounceRef
我们将创建一个 debounceRef 函数,它返回一个 customRef,在设置值时进行防抖。
import { customRef } from 'vue';
function debounceRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
// 读取时进行依赖收集
track();
return value;
},
set(newValue) {
// 清除之前的定时器
clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(() => {
value = newValue;
// 值更新后,触发依赖更新
trigger();
}, delay);
}
};
});
}实现解析:
闭包状态:
value:存储当前值,通过闭包持久化。timeout:存储setTimeout的句柄,用于清除定时器。
get 方法:
- 调用
track(),通知 Vue 当前副作用依赖于这个ref。 - 返回
value。track的调用是响应式读取的基础。
- 调用
set 方法:
- 接收
newValue。 - 清除之前的
timeout,防止过早触发。 - 创建新的定时器,在
delay毫秒后执行:- 更新
value。 - 调用
trigger(),通知所有依赖此ref的副作用重新执行。
- 更新
- 接收
四、使用示例
<template>
<div>
<input v-model="text" placeholder="输入搜索关键词..." />
<p>实时值: {{ text }}</p>
<p>防抖值: {{ debouncedText }}</p>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import { debounceRef } from './utils';
export default {
setup() {
const text = ref('');
// 创建防抖 ref,延迟 500ms
const debouncedText = debounceRef('', 500);
// 监听防抖后的值
watch(debouncedText, (val) => {
if (val) {
console.log('执行搜索:', val);
// 调用搜索 API
}
});
// 也可以在 computed 中使用
const displayText = computed(() =>
debouncedText.value ? `搜索 "${debouncedText.value}"` : '输入中...'
);
return { text, debouncedText, displayText };
}
}
</script>在这个例子中:
text是普通ref,实时响应输入。debouncedText是防抖ref,其值在用户停止输入 500ms 后才更新。watch和computed依赖于debouncedText,因此只在防抖后触发。
五、customRef 的高级应用
customRef 的能力远不止防抖。它可以实现各种自定义响应式逻辑:
1. 深度防抖对象
function deepDebounceRef(obj, delay) {
let timeout;
return customRef((track, trigger) => {
// 对整个对象进行防抖
return {
get() {
track();
return obj;
},
set(newObj) {
clearTimeout(timeout);
timeout = setTimeout(() => {
// 深度合并或替换
Object.assign(obj, newObj);
trigger();
}, delay);
}
};
});
}2. 带脏检查的 ref
function dirtyCheckRef(value) {
let old = value;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
if (newValue !== old) {
value = newValue;
old = newValue;
trigger();
}
}
};
});
}仅当新值与旧值不同时才触发更新,避免无意义的渲染。
3. 与外部存储同步
function localStorageRef(key, initialValue) {
const saved = localStorage.getItem(key);
let value = saved ? JSON.parse(saved) : initialValue;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
localStorage.setItem(key, JSON.stringify(newValue));
trigger();
}
};
});
}自动将 ref 同步到 localStorage。
六、设计哲学:暴露底层,赋能开发者
customRef 的设计体现了 Vue 3 响应式系统的一个重要理念:提供底层原语,让开发者构建高级抽象。
track 和 trigger 是响应式系统的“汇编语言”。ref、reactive 等 API 都是基于它们的高级封装。customRef 将这些原语直接暴露,使得开发者可以突破默认响应式模型的限制,实现:
- 精确的更新控制:决定何时以及是否触发更新。
- 外部系统集成:将 Vue 响应式与 DOM、Storage、WebSocket 等外部状态桥接。
- 性能优化:避免不必要的依赖追踪和触发。
七、总结
customRef 是 Vue 3 响应式工具箱中的“瑞士军刀”。通过 debounceRef 的实现,我们看到了如何利用 track 和 trigger 构建自定义的响应式行为。
关键要点:
customRef完全手动控制track和trigger。- 防抖 ref 通过延迟
trigger调用来实现更新节流。 - 闭包用于存储私有状态(如
value、timeout)。 customRef适用于需要精细控制响应式逻辑的高级场景。
掌握 customRef,意味着你不再局限于 Vue 提供的响应式模式,而是可以按需创造新的响应式原语,这是构建复杂、高性能 Vue 应用的进阶技能。