如果能让异步代码正确工作,它可以大大简化我们代码。但是,处理这种额外的复杂性,特别是与可合一起,可能会令人困惑。这篇文章介绍了无等待的异步模式。这是一种在组合中编写异步代码的方法,而不像通常那样令人头疼。
用组合API编写异步行为有时会很麻烦。所有的异步代码必须在任何反应式代码之后的设置函数的末端。如果你不这样做,它可能会干扰你的反应性。
当setup
函数运行到一个await
语句时,它将返回。一旦它返回,该组件就会被挂载,并且应用程序会像往常一样继续执行。任何在await
之后定义的响应式,无论是 computed
、watcher
,还是其他什么,都还没有被初始化。
这意味着,一个在await
之后定义的计算属性一开始不会被模板使用。相反,只有在异步代码完成,setup
函数完成执行后,它才会存在。
然而,有一种方法可以编写异步组件,可以在任何地方使用,而不需要这些麻烦。
const count = ref(0);
// 这种异步数据获取不会干扰我们的响应式
const { state } = useAsyncState(fetchData());
const doubleCount = computed(() => count * 2);
为了实现这一模式,我们将同步地挂起所有的响应式值。然后,每当异步代码完成后,这些值将被异步更新。
首先,我们需要把我们的状态准备好并返回。我们将用一个null
的值来初始化,因为我们还不知道这个值是什么。
export default useMyAsyncComposable(promise) {
const state = ref(null);
return state;
}
第二,我们创建一个方法,等待我们的 promise
,然后将结果设置为 state:
const execute = async () => {
state.value = await promise;
}
每当这个promise 返回时,它就会主动更新我们的state。
现在我们只需要把这个方法添加到组合中。
export default useMyAsyncComposable(promise) {
const state = ref(null);
// Add in the execute method...
const execute = async () => {
state.value = await promise;
}
// ...and execute it!
execute();
return state;
}
我们在从useMyAsyncComposable
方法返回之前调用了execute
函数。然而,我们并没有使用await
关键字。
当我们停止并等待execute
方法中的 promise 时,执行流立即返回到useMyAsyncComposable
函数。然后它继续执行execute()
语句并从可组合对象返回。
export default useMyAsyncComposable(promise) {
const state = ref(null);
const execute = async () => {
// 2. 等待 promise 执行完成
state.value = await promise
// 5. 一段时间后...
// Promise 执行完,state 更新
// execute 执行完成
}
// 1. 执行 `execute` 方法
execute();
// 3. await 将控制权返回到这一点上。
// 4. 返回 state 并继续执行 "setup" 方法
return state;
}
promise在后台执行,因为我们没有等待它,所以它不会在setup
函数中中断流。我们可以将此可组合放置在任何地方,而不影响响应性。
让我们看看 VueUse 中一些组合是如何实现这种模式的。
useAsyncState 可以让我们在任何地方执行任何异步方法,并获得响应性的更新结果。
const { state, isLoading } = useAsyncState(fetchData());
在查看源代码时,可以看到它实现了这种精确的模式,但具有更多的特性,并能更好地处理边界情况。
下面是 useAsyncState 的一个简化版:
export function useAsyncState(promise, initialState) {
const state = ref(initialState);
const isReady = ref(false);
const isLoading = ref(false);
const error = ref(undefined);
async function execute() {
error.value = undefined;
isReady.value = false;
isLoading.value = true;
try {
const data = await promise;
state.value = data;
isReady.value = true;
}
catch (e) {
error.value = e;
}
isLoading.value = false;
}
execute();
return {
state,
isReady,
isLoading,
error,
};
}
这个可组合的系统还返回isReady
,告诉我们数据何时被取走。我们还得到了isLoading
和error
,以跟踪我们的加载和错误状态。
现在来看看另一个可组合,我认为它有一个迷人的实现方式。
如果传给useAsyncQueue
一个 promise 函数数组,它会按顺序执行每个函数。所以,在开始下一个任务之前,会等待前一个任务的完成。为了使用更灵活,它上一个任务的结果作为输入传给下一个任务。
const { result } = useAsyncQueue([getFirstPromise, getSecondPromise]);
下面是一个官网的例子:
const getFirstPromise = () => {
// Create our first promise
return new Promise((resolve) => {
setTimeout(() => {
resolve(1000);
}, 10);
});
};
const getSecondPromise = (result) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1000 + result);
}, 20);
});
};
const { activeIndex, result } = useAsyncQueue([
getFirstPromise,
getSecondPromise
]);
即使它在异步执行代码,我们也不需要使用await
。即使在内部,可组合的程序也不使用await
。相反,我们在 "后台"执行这些 promise,并让结果响应式更新。
让我们看看这个组合是如何工作的。
// 初始一些默认值
const initialResult = Array.from(new Array(tasks.length), () => ({
state: promiseState.pending,
data: null,
});
// 将默认值变成响应式
const result = reactive(initialResult);
// 声明一个响应式的下标
const activeIndex = ref(-1);
主要的功能是由一个reduce
来支持的,它逐个处理每个功能
tasks.reduce((prev, curr) => {
return prev.then((prevRes) => {
if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
onFinished();
return;
}
return curr(prevRes).then((currentRes) => {
updateResult(promiseState.fulfilled, currentRes);
activeIndex.value === tasks.length - 1 && onFinished();
return currentRes;
})
}).catch((e) => {
updateResult(promiseState.rejected, e);
onError();
return e;
})
}, Promise.resolve());
Reduce 方法有点复杂,我们拆解一下,一个个看:
tasks.reduce((prev, curr) => {
// ...
}, Promise.resolve());
然后,开始处理每个任务。通过在前一个promise基础上链接一个.then
来完成这个任务。如果promise 被拒绝,就提前中止并返回。
if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
onFinished();
return;
}
如果不提前终止,则执行下一个任务,并传递上一个 promise 的结果。我们还调用updateResult
方法,将其添加到该组合返回的 result
数组中
return curr(prevRes).then((currentRes) => {
updateResult(promiseState.fulfilled, currentRes);
activeIndex.value === tasks.length - 1 && onFinished();
return currentRes;
});
正如你所看到的,该可组合实现了Async Without Await模式,但该模式只是整个可组合的几行。所以它不需要很多额外的工作,只要记住把它放在适当的位置
如果我们使用Async Without Await模式,我们可以更容易地使用异步组合。这种模式可以让我们把异步代码放在我们想放的地方,而不用担心破坏响应应性。