☰
VueUse computedAsync 使用与原理指南:在 Vue 3 中创建响应式异步计算
2026/10/5 6:36:38 网站建设 项目流程
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

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 )

这段代码做了三件事:

  1. 建立依赖:求值回调中同步读取了name.value,因此name成为userInfo的依赖;
  2. 自动重求值:name变化后,computedAsync自动重新执行异步求值并更新结果;
  3. 提供过渡态:在第一次请求返回前,结果值保持为null。

值得注意的是,computedAsync默认非懒加载:创建时立即开始第一次求值(index.browser.test.ts 中it('is not lazy by default')验证了func在创建时即被调用一次),这与 Vue 内置computed的懒求值行为不同。

完整 API 与选项详解

computedAsync的完整选项接口继承自ConfigurableFlushSync(定义于 packages/shared/utils/types.ts),并扩展了以下字段:

选项类型默认值作用
lazybooleanfalse是否在首次访问.value时才触发求值
evaluatingRef<boolean>undefined传入一个 ref,实时接收「是否正在求值」的状态
shallowbooleantrue内部使用shallowRef(浅层响应);设为false改用深层ref
onError(e: unknown) => voidglobalThis.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):

  1. 用户在回调里调用onCancel(清理函数),VueUse 内部把它包装进onInvalidate;
  2. 当依赖变化触发重求值时,上一次副作用被失效,onInvalidate注册的清理函数被执行;
  3. 执行时会先把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的关键差异,务必牢记:

  1. 只有第一层调用栈中同步访问的依赖才会触发重求值。computedAsync与 Vue 的computed一样做依赖追踪并在依赖变化时自动重算,但异步阶段访问的依赖不会被追踪——例如await fetch(...)之后才读取的 ref,其变化不会触发重求值。因此必须在回调开头(第一层同步调用栈中)同步读取所有依赖值。

  2. 依赖变化即触发重求值,与结果是否正被读取无关。与 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

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载
上一篇:Grommet终极指南:5大核心功能构建现代化React应用
下一篇:React Native底部弹窗与React Navigation无缝集成终极指南:打造流畅导航体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询