如何在 Vue3 中异步使用 computed 计算属性
前言
众所周知,Vue 中的 computed 计算属性默认必须同步调用,这也就意味着,所有值都必须立即返回,如果试图异步调用,那么 Vue 会立刻报错。
但是这很显然是不符合我们的一部分需求的:例如,我想通过 fetch
函数从后端调取数据,然后返回到 computed 中,这个时候 Vue 自带的 computed 就没法满足我们的需求了。
当然这并不是说这种情况就毫无解法了,我们完全可以创建一个 reactive
对象或 ref
引用,然后在组件 onMounted
生命周期手动为这个对象赋值,也可以解决问题,但是略显繁琐,也不够优雅。
一个偶然的机会,我看到了一个更好的解决方案。
useAsyncComputed
函数
我是在 GitHub Gist 中看到的由一位名为 loilo 的用户在两年前发布的 Gist,名为 Async Computed Values for Vue 3。在这个 Gist 中介绍了一种基于 Vue 3.0 和 TypeScript 4.0 的名为 useAsyncComputed
的组合式 API 函数。
要想使用这个函数,只需要将下方的代码引入你的项目:
代码语言:javascript复制import { ref, readonly, watchEffect, Ref, DeepReadonly } from 'vue'
/**
* Handle overlapping async evaluations
*
* @param cancelCallback The provided callback is invoked when a re-evaluation of the computed value is triggered before the previous one finished
*/
export type AsyncComputedOnCancel = (cancelCallback: () => void) => void
export type AsyncComputedResult<T> = [
value: DeepReadonly<Ref<T>>,
evaluating: DeepReadonly<Ref<boolean>>
]
/**
* Create an asynchronous computed dependency
*
* @param callback The promise-returning callback which generates the computed value
* @param defaultValue A default value, used until the first evaluation finishes
* @returns A two-item tuple with the first item being a readonly ref to the computed value and the second item holding a boolean ref, indicating whether the async computed value is currently (re-)evaluated
*/
export default function useAsyncComputed<T>(
callback: (onCancel: AsyncComputedOnCancel) => T | Promise<T>,
defaultValue?: T
): AsyncComputedResult<T> {
let counter = 0
const current = ref(defaultValue) as Ref<T>
const evaluating = ref<boolean>(false)
watchEffect(async onInvalidate => {
counter
const counterAtBeginning = counter
let hasFinished = false
try {
// Defer initial setting of `evaluating` ref
// to avoid having it as a dependency
Promise.resolve().then(() => {
evaluating.value = true
})
const result = await callback(cancelCallback => {
onInvalidate(() => {
evaluating.value = false
if (!hasFinished) {
cancelCallback()
}
})
})
if (counterAtBeginning === counter) {
current.value = result
}
} finally {
evaluating.value = false
hasFinished = true
}
})
return [readonly(current), readonly(evaluating)]
}
它的用法也非常简单:
代码语言:javascript复制import { ref } from 'vue'
import useAsyncComputed from './use-async-computed'
const packageName = ref('color-blend')
function getDownloads() {
return fetch(`https://api.npmjs.org/downloads/point/last-week/${packageName.value}`)
.then(response => response.json())
.then(result => result.downloads)
}
const [downloads] = useAsyncComputed(getDownloads, 0)
此处的 downloads 变量即可像 computed
一样使用,并会随上游数据变化自动更新。可以看到,通过引入 useAsyncComputed
,我们可以在异步的场景下获得我们想要的数据。
那么接下来,我们具体了解一下这个 useAsyncComputed
函数的使用:
首先,这个函数有两个参数,第一个参数 callback: (onCancel: AsyncComputedOnCancel) => T | Promise<T>
,可传入异步函数;第二个参数 defaultValue?: T
,则是当异步调用未完成时该 computed 属性的默认值。
其次,这个函数的返回值实际上是一个大小为 2 的数组,数组的第一个元素为当前的运算值,第二个元素则是异步调用是否已返回。正因为此,可以看到上方的示例中我们使用了 JavaScript 的解构语法来从 useAsyncComputed
的值,而不是直接赋值。
更好的是,这个 useAstncComputed
函数还允许为取消事件做出响应,具体的方法可以参考原 Gist 给出的示例。
更好的解决方案
事实上如上所述,这个 Gist 已经是两年前的作品了,那么两年后,是否有更方便的解决方案?
答案是有的,在于原作者的交谈中,我得知我们可以通过引入 VueUse 这个库并使用其中自带的 computedAsync 函数来达到相同的效果。这个函数的使用方法与上方介绍的函数大同小异,并且提供了更多功能(例如懒加载),具体信息可以参看其文档。