跳到主要内容

为什么 computed 里不应该做异步?要异步怎么写?

面试速答(30 秒版 TL;DR)

  • computed 的定位是“同步派生状态”:执行 getter 时同步读取依赖,立刻返回值,并利用缓存减少重复计算。
  • 异步(await/Promise)会破坏这套模型:
    • 依赖收集只发生在 getter 的同步执行阶段await 之后的读取不会被追踪。
    • computed 需要返回“可直接使用的值”,而不是 Promise;Promise resolve 不会自动触发 computed 更新。
  • 需要异步时用:watch / watchEffect 把异步结果写进 ref,再用 computed 做同步派生;或者用成熟封装(如 VueUse 的 computedAsync / useAsyncState)。

computed 的核心机制:同步依赖收集 + 缓存

可以把 computed 理解成:

  1. 运行 getter(同步)。
  2. 在运行过程中“读到了哪些响应式数据”,就把它们记录为依赖。
  3. 依赖变化时把 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、埋点)会让依赖关系变得不透明,排查更新问题很痛苦。