关于 Vue 中 customRef 的一些奇思妙想
响应式数据更新带来的副作用是立即执行的,可以考虑更换调度策略或使用队列来延缓副作用的执行(比如组件的异步渲染)。但想要让响应式数据的自身的更新延迟执行或条件执行,则需要自己定义,Vue 中提供了 customRef
。
具有防抖的 ref
比如实现值更新后,延迟 2 秒再发出通知更新页面,并且合并重复通知。这样可以避免页面被频繁更新。
function debouncedRef<T>(value: T, timeout = 0): Ref<T> {
let timeoutId: number | null = null;
return customRef((track, trigger) => ({
get() {
// 添加到依赖追踪
track();
return value;
},
set(newValue) {
value = newValue;
if (timeoutId) {
clearTimeout(timeoutId);
}
// 通过设置超时为 0 可以触发异步的调度
// 默认 `watchEffect` 的回调是同步触发的
// setTimeout(trigger);
// 通过 setTimeout 设定时间则是延迟更新
timeoutId = setTimeout(trigger, timeout);
},
}));
}
const msg = debouncedRef('debouncedRef', 2_000);
<button @click="msg += '$'">{{ msg }}</button>
单个订阅者
只有首个数据使用者才会收到更新事件(推模型),虽然后续使用者可以访问到最新数据,但并不知道何时数据更新了(拉模型)。依赖追踪是可选的,因此可以选择在特定的上下文中关闭依赖追踪,非常灵活。
function firstRef<T>(value: T): Ref<T> {
let tracked = false;
return customRef((track, trigger) => ({
get() {
if (!tracked) {
tracked = true;
track();
}
return value;
},
set(newValue) {
value = newValue;
trigger();
},
}));
}
const msg = firstRef('first');
watchEffect(() => {
console.log(msg.value);
});
<button @click="msg = 'second'">{{ msg }}</button>
这样页面的内容就不会更新了。但是如果不写 watchEffect
,在模板中多次使用 msg
,那么他们还是会一起更新:
<button @click="msg = 'second'">{{ msg }}</button>
<pre>{{ msg }}</pre>
<pre>{{ msg }}</pre>
因为组件渲染是一个整体,只要有一个订阅更新,页面的其他部分都会被动地拿到最新值。
实现类似 switchMap
的效果
再比如实现 switchMap
或者说在 onCleanup
中使回调失效的效果:
<script setup lang="ts">
import { customRef, type Ref } from 'vue';
const entries = ['React', 'Vue', 'Angular', 'Svelte', 'Solid', 'Lit'];
async function fetchSuggestions(keyword = ''): Promise<string[]> {
keyword = keyword.toLowerCase();
await new Promise(resolve => setTimeout(resolve, 1_000));
return entries.filter(e => e.toLowerCase().includes(keyword));
}
function switchAsyncRef<T>(task: Promise<T>): Ref<T | null> {
let id = 0;
let value: T | null = null;
const myRef = customRef((track, trigger) => ({
get() {
track();
return value;
},
async set(newValue) {
const currentId = ++id;
const resolvedValue = await newValue;
if (currentId !== id) return;
value = resolvedValue;
trigger();
},
}));
myRef.value = task as unknown as typeof myRef.value;
return myRef;
}
const suggestions = switchAsyncRef(fetchSuggestions());
</script>
<template>
<input
type="text"
@input="
(suggestions as unknown as Promise<typeof suggestions>) =
fetchSuggestions(($event.target as HTMLInputElement).value)
"
/>
<ol>
<li v-for="suggestion in suggestions">{{ suggestion }}</li>
</ol>
</template>
上面的代码用了两次类型断言,这是因为 Vue 的类型系统没有考虑到 Ref#value
的 get
、set
函数类型可以不一致,源码。即便补上相关定义,终端中不会报错,但 VS Code 中的类型提示依旧有问题,可能还需要更新 Volar 的部分代码。