为什么 computed 里不应该做异步?要异步怎么写?
面试速答(30 秒版 TL;DR)
- computed 的定位是“同步派生状态”:执行 getter 时同步读取依赖,立刻返回值,并利用缓存减少重复计算。
- 异步(
await/Promise)会破坏这套模型:- 依赖收集只发生在 getter 的同步执行阶段,
await之后的读取不会被追踪。 - computed 需要返回“可直接使用的值”,而不是 Promise;Promise resolve 不会自动触发 computed 更新。
- 依赖收集只发生在 getter 的同步执行阶段,
- 需要异步时用:
watch/watchEffect把异步结果写进ref,再用 computed 做同步派生;或者用成熟封装(如 VueUse 的computedAsync/useAsyncState)。
computed 的核心机制:同步依赖收集 + 缓存
可以把 computed 理解成:
- 运行 getter(同步)。
- 在运行过程中“读到了哪些响应式数据”,就把它们记录为依赖。
- 依赖变化时把 computed 标记为 dirty,下次取值再重新算。
这个模型要求 getter 同步且尽量纯:只做计算,不做副作用。
为什么异步会让 computed 失效
1) await 会把 getter 拆成两段,依赖收集不完整
依赖收集只覆盖“第一次同步跑完的那段”。如果你写:
const x = computed(async () => {
await something();
return state.value; // 这次读取可能不会被收集进依赖
});
那么 state.value 的读取发生在 await 之后,它不一定处于响应式追踪上下文中,computed 就不知道该在 state 变化时重新计算。
2) computed 需要“值”,Promise resolve 不是响应式事件
即使你返回 Promise,模板/调用方拿到的是 Promise 本身;Promise resolve 并不会自动触发 Vue 的响应式更新(除非你把 resolve 的结果写回某个 ref)。
正确做法:异步放到 watchEffect,结果写进 ref,再用 computed 派生
这是最通用、可解释、可控的写法。
import { computed, ref, watchEffect } from 'vue';
type User = { id: string; name: string };
const userId = ref('1');
const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<unknown>(null);
watchEffect((onCleanup) => {
const id = userId.value;
const controller = new AbortController();
onCleanup(() => controller.abort());
loading.value = true;
error.value = null;
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
user.value = data;
})
.catch((e) => {
// AbortError 通常可以忽略,这里略
error.value = e;
})
.finally(() => {
loading.value = false;
});
});
// computed 只做同步派生
const userLabel = computed(() => (user.value ? `${user.value.name}(${user.value.id})` : '-'));
典型追问
Q1:那我能不能让 computed 返回 Promise,然后在模板里 await?
不行。模板渲染是同步的,Vue 不会在模板表达式里帮你 await Promise。要么用 <Suspense> 承接“异步 setup”,要么把结果写进 ref 再渲染。
Q2:用 watch 还是 watchEffect?
- 依赖源明确、想拿到
newVal/oldVal:用watch。 - 依赖源可能不止一个或会变化、想自动收集依赖:用
watchEffect,并用onCleanup处理竞态。
易错点/坑
- 把 computed 当成“异步请求容器”,会导致缓存、竞态、错误处理、loading 状态都难以管理。
- computed 里做副作用(请求、写 storage、埋点)会让依赖关系变得不透明,排查更新问题很痛苦。