先记住这个答案
异步组合式函数应先约定请求开始、成功、失败和失效时的状态转换,再为每次请求保留独立身份或有效标记。参数变化时清理旧请求,既尝试取消外部工作,也阻止晚到结果覆盖当前 data、error 和 loading。旧请求的 finally 同样需要检查有效性,否则可能提前关闭新请求的加载状态。是否保留旧数据、怎样显示刷新失败,以及空参数是否请求,都应写进接口契约。
- data、error、loading 必须属于同一当前请求语境
- 取消请求与忽略过期结果是互补措施
- 成功、失败、finally 和卸载清理都需要考虑竞态
先约定界面状态的含义
搜索列表更新关键词时,可以清空旧结果,也可以保留旧数据并显示刷新中,两种体验都合理,但 data 当前属于哪个请求必须清楚。错误出现后是否保留旧结果,也应有明确规则。不要用 data 是否为真来推断是否成功,因为空数组或合法空值可能就是成功结果。
下面使用可注入的加载函数,方便在测试中控制完成顺序,而不是固定连接某个后端。每次 watch 执行先建立有效标记和清理函数,空键不发请求;新请求开始时清空旧数据和错误,保持状态转换可预测。
import { ref, shallowRef, toValue, watch } from 'vue';
import type { MaybeRefOrGetter } from 'vue';
function useRemote<T>(source: MaybeRefOrGetter<string>,
load: (key: string, signal: AbortSignal) => Promise<T>) {
const data = shallowRef<T | null>(null);
const error = shallowRef<Error | null>(null);
const loading = ref(false);
watch(() => toValue(source), async (key, _, onCleanup) => {
let active = true;
const controller = new AbortController();
onCleanup(() => {
active = false;
controller.abort();
loading.value = false;
});
data.value = null;
error.value = null;
if (!key) { loading.value = false; return; }
loading.value = true;
try {
const result = await load(key, controller.signal);
if (active) data.value = result;
} catch (cause) {
if (active) error.value = cause instanceof Error ? cause : new Error(String(cause));
} finally {
if (active) loading.value = false;
}
}, { immediate: true });
return { data, error, loading };
}在组件 setup 的同步调用链或明确管理的 effectScope 中使用。加载器可以响应 signal 取消,也可能忽略取消;active 标记仍会阻止旧结果覆盖新请求。
为什么只调用 abort 还不够
取消能否及时生效取决于底层工作,结果也可能已经进入后续处理。因此应让写入操作同时检查当前请求是否有效。旧请求失败时不能覆盖新请求的错误状态,旧 finally 也不能把新请求仍在进行的 loading 改成 false。
测试可以让请求 A 先启动、B 后启动却先完成,再让 A 晚到,确认最终仍保留 B 的数据。还应反过来让旧请求先结束而新请求未完成,检查加载状态不会提前结束。这些顺序比只测试一次正常成功更能说明实现是否可靠。
清理、错误与 SSR 仍有边界
在组件或效果作用域结束时,watch 的清理会使当前请求失效并尝试取消。若在没有明确作用域的任意异步回调里创建 watcher,就需要自己管理停止行为。加载器还应按后端协议处理非成功状态与数据解析,组合式函数不会自动判断返回内容是否符合业务结构。
客户端封装不等于完整 SSR 数据方案。服务端首屏取数、缓存键和客户端避免重复请求,需要与所用框架的数据机制协调。若加入重试或手动刷新,还要明确它们是否产生新请求身份,不能绕过同一套状态与失效规则。
容易答错的地方
- 只给 data 写入加有效性检查
- error 和 finally 也可能来自过期请求,必须一起约束,才能保持状态一致。排查“Vue 异步组合式函数 loading error data 请求竞态”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
- 用了 AbortController 就保证旧任务绝不完成
- 底层实现可能忽略取消或已进入后续阶段,仍需要拒绝过期结果的逻辑。排查“Vue 异步组合式函数 loading error data 请求竞态”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
面试官还会怎么问?
新请求开始一定要清空旧数据吗?
不是,保留旧数据适合刷新体验,但要同时记录当前请求键和刷新状态,避免让用户误认数据属于新条件。
为什么把 load 作为参数传入?
可以分离网络协议与状态管理,也方便用可控 Promise 测试竞态和错误,不必依赖真实网络速度。
loading 能表示所有异步阶段吗?
简单请求可以,复杂流程可能还需区分初次加载、刷新、取消或重试;状态命名应反映实际用户需要。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。