- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
computedAsync是 VueUse 响应式工具集中用于「异步计算」的核心 composable:它像 Vue 内置computed一样声明式地跟踪依赖,同时允许你传入返回 Promise 的异步求值函数,并原生支持取消(onCancel)、求值状态追踪(evaluating)、懒加载(lazy)与错误处理(onError)。读完本文,你将掌握在 Vue 3 / Nuxt 3 项目中用computedAsync替代手写watch + ref的完整实战方案,并深入理解其底层实现与边界限制。
computedAsync 是什么
在 Vue 3 中,内置computed只能处理同步求值;面对「依赖某个响应式值、却需要异步获取结果」的场景(请求用户信息、查询下载量、拉取远程配置等),开发者通常不得不手写watch搭配普通ref,手动处理时序、竞态与取消逻辑。VueUse 的computedAsync正是为这一场景提供的声明式答案,其文档定义只有一句话:Computed for async functions(为异步函数设计的计算属性)。
computedAsync属于 Reactivity 分类,以 packages/core/index.ts 中的export * from './computedAsync'从@vueuse/core统一导出,同时保留旧别名asyncComputed(已标记@deprecated,见 packages/core/computedAsync/index.ts)。函数完整定义与文档位于 packages/core/computedAsync/index.ts 与 packages/core/computedAsync/index.md。
基础用法:三步接入异步计算
computedAsync的基本签名是:computedAsync(求值回调, 初始值, 选项)。第一个参数接收一个onCancel回调(用于注册取消逻辑),返回T | Promise<T>;第二个参数是首个求值完成前使用的初始状态。
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const name = shallowRef('jack') const userInfo = computedAsync( async () => { return await mockLookUp(name.value) }, null, // initial state:第一次求值完成前,userInfo.value === null )这段代码做了三件事:
- 建立依赖:求值回调中同步读取了
name.value,因此name成为userInfo的依赖; - 自动重求值:
name变化后,computedAsync自动重新执行异步求值并更新结果; - 提供过渡态:在第一次请求返回前,结果值保持为
null。
值得注意的是,computedAsync默认非懒加载:创建时立即开始第一次求值(index.browser.test.ts 中it('is not lazy by default')验证了func在创建时即被调用一次),这与 Vue 内置computed的懒求值行为不同。
完整 API 与选项详解
computedAsync的完整选项接口继承自ConfigurableFlushSync(定义于 packages/shared/utils/types.ts),并扩展了以下字段:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
lazy | boolean | false | 是否在首次访问.value时才触发求值 |
evaluating | Ref<boolean> | undefined | 传入一个 ref,实时接收「是否正在求值」的状态 |
shallow | boolean | true | 内部使用shallowRef(浅层响应);设为false改用深层ref |
onError | (e: unknown) => void | globalThis.reportError ?? noop | 异步求值抛出错误时的回调 |
flush | 'pre' \| 'post' \| 'sync' | 'sync' | 依赖变化的监听时机(继承自ConfigurableFlushSync) |
此外,第二个参数支持两种形态:既可以是普通对象选项,也可以直接传入一个Ref<boolean>——此时它等价于{ evaluating: 该Ref }。源码中通过isRef(optionsOrRef)判断并归一化(index.ts)。
flush 时机的特殊之处
flush默认值为'sync',这与 Vue 原生watch默认的'pre'不同(ConfigurableFlushSync的类型注释明确说明这一点)。从实现看,内部使用watchEffect(..., { flush })(index.ts),即依赖变化时同步触发重求值调度,让异步计算结果能更及时地反映依赖变更。
追踪求值状态(Evaluation State)
当异步函数执行时间较长时,UI 通常需要展示 loading 状态。computedAsync允许传入一个evaluatingref 来追踪「当前是否正在求值」,既可作为第三个参数直接传入,也可放进 options 中:
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const evaluating = shallowRef(false) const userInfo = computedAsync( async () => { /* your logic */ }, null, evaluating, // 等价于 { evaluating } 作为第三个参数 )// 或者通过 options 传入 const userInfo = computedAsync( async () => { /* your logic */ }, null, { evaluating }, )模板中即可直接用v-if="evaluating"渲染加载提示。官方 demo(packages/core/computedAsync/demo.vue)正是用evaluating配合stringify(state)展示「Evaluating: true/false」状态,并借id变化触发对 jsonplaceholder 接口的重新请求。
从源码看,evaluating的状态切换有两个实现细节(index.ts):
- 求值开始时,通过
Promise.resolve().then(() => evaluating.value = true)延迟一拍赋值,避免evaluating自身成为被追踪的依赖而引发循环; - 求值结束(成功或失败)且该次求值仍是最新一次时,在
finally中把evaluating重置为false(index.ts)。
取消过期请求:onCancel 与 AbortController
当依赖在异步函数 resolve 之前再次变化时,旧请求已经过期,继续等待它的结果会造成竞态。computedAsync的求值回调会注入一个onCancel函数,用来注册「被新求值顶掉时」要执行的清理逻辑。官方示例展示了与 fetch 的AbortController配合的完整写法:
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const packageName = shallowRef('@vueuse/core') const downloads = computedAsync(async (onCancel) => { const abortController = new AbortController() onCancel(() => abortController.abort()) return await fetch( `https://api.npmjs.org/downloads/point/last-week/${packageName.value}`, { signal: abortController.signal }, ) .then(response => response.ok ? response.json() : { downloads: '—' }) .then(result => result.downloads) }, 0)这里每次重新求值都会创建一个新的AbortController;当packageName在上一次 fetch 完成前改变,onCancel注册的abortController.abort()就会被调用,从物理上终止过期网络请求,避免旧结果覆盖新值。
底层取消机制
结合源码看,取消是通过 Vue 的watchEffect副作用失效机制(onInvalidate)实现的(index.ts):
- 用户在回调里调用
onCancel(清理函数),VueUse 内部把它包装进onInvalidate; - 当依赖变化触发重求值时,上一次副作用被失效,
onInvalidate注册的清理函数被执行; - 执行时会先把
evaluating置为false,且仅当上一次求值尚未结束(!hasFinished)时才调用用户的取消回调。
AsyncComputedOnCancel的类型定义为(cancelCallback: Fn) => void,注释明确:传入的回调会在「上一次求值尚未完成、新一次重求值已触发」时被调用(index.ts)。
测试用例cancel is called与cancel is called for lazy(index.browser.test.ts)验证了:只有当旧求值未结束时依赖再次变化,取消回调才会触发;若上一次已 resolve,则不会误触发取消。
懒加载:lazy 模式
默认computedAsync在创建时立即开始求值。若希望「首次访问.value时才触发第一次求值」,设置lazy: true:
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const evaluating = shallowRef(false) const userInfo = computedAsync( async () => { /* your logic */ }, null, { lazy: true, evaluating }, )源码中 lazy 的实现是:内部维护startedref(初始为!lazy),非 lazy 时直接返回内部currentref;lazy 时则包一层computed(() => { started.value = true; return current.value }),一旦有人读取.value,就置started为true并触发watchEffect开始求值(index.ts)。测试is lazy if configured(index.browser.test.ts)验证了:lazy 模式下创建时func不被调用,首次读取data.value后才触发。
另外注意类型差异:lazy 模式下返回ComputedRef<T>/ComputedRef<T | undefined>,非 lazy 模式返回Ref<T>/Ref<T | undefined>(测试types are correct when lazy与type when lazy is a boolean在 index.browser.test.ts 中通过expectTypeOf断言)。
错误处理:onError
异步求值抛出的异常默认不会冒泡破坏组件,而是被捕获并交给onError。可自定义回调记录或上报错误:
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const name = shallowRef('jack') const userInfo = computedAsync( async () => { return await mockLookUp(name.value) }, null, { onError(e) { console.error('Failed to fetch user info', e) }, }, )实现上,求值回调在try/catch中执行,异常统一走onError(e)(index.ts);出错后current保持原值不变,不会用异常污染结果。默认的onError是globalThis.reportError ?? noop(index.ts),即优先调用 Web 平台的全局错误上报 API。测试用例验证了两点(index.browser.test.ts):
- 默认行为:错误会被转发给
globalThis.reportError; - 自定义行为:传入
onError后能收到原始错误对象并读取e.message,此时结果值保持undefined。
浅层 vs 深层响应:shallow 选项
computedAsync内部默认使用shallowRef保存结果,即只对最外层引用做响应式处理。若你的异步结果本身是嵌套结构,且需要深层响应(例如模板深层访问对象的属性并希望其变更触发更新),可设shallow: false:
import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' const name = shallowRef('jack') const userInfo = computedAsync( async () => { return await fetchNestedData(name.value) }, null, { shallow: false }, // enables deep reactivity )对应源码为const current = (shallow ? shallowRef(initialState) : deepRef(initialState))(index.ts),其中deepRef即 Vue 的ref。默认浅层的主要收益是性能:异步结果通常整体替换,浅层 ref 避免了深层对象递归代理的开销。
源码级原理:并发求值的防竞态机制
computedAsync最精巧的部分是只采用最新一次求值的结果。考虑一个典型竞态:依赖快速连续变化 N 次,会发起 N 个并发异步请求,先发出的请求可能后返回。若直接覆盖,旧请求会覆盖新结果。源码用递增计数器解决:
let counter = 0 watchEffect(async (onInvalidate) => { // ... counter++ const counterAtBeginning = counter // ... try { const result = await evaluationCallback(...) if (counterAtBeginning === counter) current.value = result // 仅当本次求值仍是最新一次才写入 } catch (e) { onError(e) } // ... })(见 index.ts)每次求值开始时记下当时的counter,resolve 后对比counterAtBeginning === counter才允许写入结果,从而保证「过期请求永不覆盖最新结果」。测试uses last result(index.browser.test.ts)精确复现了这一场景:依赖从 1 连续变为 2、3、4,即使 2 与 3 对应的旧请求最后才 resolve,最终值仍是依赖为 4 时的8。
另外,computedAsync的值是一个真正的响应式引用,可以被其他computed继续派生。测试triggers(index.browser.test.ts)展示了computed(() => (double.value ?? 0) + 1)会随异步结果更新而连锁刷新。
注意事项(Caveats)
官方文档明确列出两点与 Vue 内置computed的关键差异,务必牢记:
只有第一层调用栈中同步访问的依赖才会触发重求值。
computedAsync与 Vue 的computed一样做依赖追踪并在依赖变化时自动重算,但异步阶段访问的依赖不会被追踪——例如await fetch(...)之后才读取的 ref,其变化不会触发重求值。因此必须在回调开头(第一层同步调用栈中)同步读取所有依赖值。依赖变化即触发重求值,与结果是否正被读取无关。与 Vue 内置
computed相反(内置 computed 只在结果被追踪时才求值),computedAsync只要依赖变化就会重新求值,无论其返回值当前是否被任何地方读取。这对「立即开始求值、结果常驻」的使用方式(如预取数据)更合适,但也意味着它不会像内置 computed 那样按需懒求值——若需要按需求值,请显式设置lazy: true。
TypeScript 类型声明
computedAsync有 4 个重载,核心类型如下(完整声明见 packages/core/computedAsync/index.ts):
export type AsyncComputedOnCancel = (cancelCallback: Fn) => void export interface AsyncComputedOptions<Lazy = boolean> extends ConfigurableFlushSync { /** 是否懒求值,默认 false */ lazy?: Lazy /** 接收异步求值状态的 ref */ evaluating?: Ref<boolean> /** 是否使用 shallowRef,默认 true */ shallow?: boolean /** 捕获到错误时的回调 */ onError?: (e: unknown) => void } computedAsync<T>(evaluationCallback, initialState, optionsOrRef: AsyncComputedOptions<true>): ComputedRef<T> computedAsync<T>(evaluationCallback, initialState: undefined, optionsOrRef: AsyncComputedOptions<true>): ComputedRef<T | undefined> computedAsync<T>(evaluationCallback, initialState: T, optionsOrRef?: Ref<boolean> | AsyncComputedOptions): Ref<T> computedAsync<T>(evaluationCallback, initialState?: undefined, optionsOrRef?: Ref<boolean> | AsyncComputedOptions): Ref<T | undefined>类型层面的返回值随lazy与是否提供initialState联动(ComputedRefvsRef、TvsT | undefined),TypeScript 可以据此在编译期约束你的取值与空值判断。
总结与最佳实践
computedAsync把「依赖响应式状态 + 异步获取 + 竞态取消 + 状态追踪 + 错误兜底」封装成一个声明式原语,取代了手写的watch样板代码。实战中建议遵循以下模式:
- 在求值回调第一行同步读取所有依赖的 ref,确保依赖能被正确追踪;
- 涉及网络请求时,始终用
AbortController+onCancel终止过期请求; - 用
evaluatingref 驱动 loading UI,用onError收敛异常,避免请求失败导致未捕获的 Promise 拒绝; - 高频更新的异步结果保持默认
shallow: true;只有确实需要深层响应嵌套结果时才设置shallow: false; - 若结果仅在特定时机才被消费,用
lazy: true延迟首次请求。
如需进一步验证行为,可直接阅读本仓库的 单元测试(覆盖懒加载、竞态、取消、错误、类型推断等全部关键路径)与 官方 demo(完整展示evaluating+onCancel+ fetch 的组合用法)。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
airi 项目 Vue 异步计算实战:VueUse `computedAsync` 的原理、用法与工程落地
airi 项目 Vue 异步计算实战:VueUse computedAsync 的原理、用法与工程落地 本篇指南以仓库 .agents/skills/vueus
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染VueUse computedAsync 完全指南:为 Vue 3 打造可靠的异步计算属性
VueUse computedAsync 完全指南:为 Vue 3 打造可靠的异步计算属性 computedAsync 是 VueUse 中用于处理异步计算的核
前端VueUse useAsyncState 实战指南:在 Vue 3 中以响应式方式管理异步状态
VueUse useAsyncState 实战指南:在 Vue 3 中以响应式方式管理异步状态 导读 useAsyncState 是 VueUse 核心包( @
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考