参考答案:
大家在使用 useEffect
的时候,假如回调函数中使用 async...await...
的时候,会报错如下。
看报错,我们知道 effect function
应该返回一个销毁函数(return
返回的 cleanup
函数),如果 useEffect
第一个参数传入 async
,返回值则变成了 Promise
,会导致 react
在调用销毁函数的时候报错**。
useEffect
作为 Hooks
中一个很重要的 Hooks
,可以让你在函数组件中执行副作用操作。
它能够完成之前 Class Component
中的生命周期的职责。它返回的函数的执行时机如下:
不管是哪个,我们都不希望这个返回值是异步的,这样我们无法预知代码的执行情况,很容易出现难以定位的 Bug。
所以 React 就直接限制了不能 useEffect 回调函数中不能支持 async...await...
竟然 useEffect 的回调函数不能使用 async...await
,那我直接在它内部使用。
做法一:创建一个异步函数(async...await
的方式),然后执行该函数。
1useEffect(() => { 2 const asyncFun = async () => { 3 setPass(await mockCheck()); 4 }; 5 asyncFun(); 6}, []);
做法二:也可以使用 IIFE
,如下所示:
1useEffect(() => { 2 (async () => { 3 setPass(await mockCheck()); 4 })(); 5}, []);
既然知道了怎么解决,我们完全可以将其封装成一个 hook,让使用更加的优雅。我们来看下 ahooks 的 useAsyncEffect
,它支持所有的异步写法,包括 generator function
。
思路跟上面一样,入参跟 useEffect 一样,一个回调函数(不过这个回调函数支持异步),另外一个依赖项 deps。内部还是 useEffect,将异步的逻辑放入到它的回调函数里面。
1function useAsyncEffect( 2 effect: () => AsyncGenerator<void, void, void> | Promise<void>, 3 // 依赖项 4 deps?: DependencyList, 5) { 6 // 判断是 AsyncGenerator 7 function isAsyncGenerator( 8 val: AsyncGenerator<void, void, void> | Promise<void>, 9 ): val is AsyncGenerator<void, void, void> { 10 // Symbol.asyncIterator: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator 11 // Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。 12 return isFunction(val[Symbol.asyncIterator]); 13 } 14 useEffect(() => { 15 const e = effect(); 16 // 这个标识可以通过 yield 语句可以增加一些检查点 17 // 如果发现当前 effect 已经被清理,会停止继续往下执行。 18 let cancelled = false; 19 // 执行函数 20 async function execute() { 21 // 如果是 Generator 异步函数,则通过 next() 的方式全部执行 22 if (isAsyncGenerator(e)) { 23 while (true) { 24 const result = await e.next(); 25 // Generate function 全部执行完成 26 // 或者当前的 effect 已经被清理 27 if (result.done || cancelled) { 28 break; 29 } 30 } 31 } else { 32 await e; 33 } 34 } 35 execute(); 36 return () => { 37 // 当前 effect 已经被清理 38 cancelled = true; 39 }; 40 }, deps); 41}
async...await
我们之前已经提到了,重点看看实现中变量 cancelled
的实现的功能。
它的作用是中断执行。
通过
yield
语句可以增加一些检查点,如果发现当前effect
已经被清理,会停止继续往下执行。
试想一下,有一个场景,用户频繁的操作,可能现在这一轮操作 a 执行还没完成,就已经开始开始下一轮操作 b。这个时候,操作 a 的逻辑已经失去了作用了,那么我们就可以停止往后执行,直接进入下一轮操作 b 的逻辑执行。这个 cancelled
就是用来取消当前正在执行的一个标识符。
可以看到上面的 useAsyncEffect
,内部的 useEffect
返回函数只返回了如下:
1return () => { 2 // 当前 effect 已经被清理 3 cancelled = true; 4};
这说明,你通过 useAsyncEffect 没有 useEffect 返回函数中执行清除副作用的功能。
你可能会觉得,我们将 effect
(useAsyncEffect
的回调函数)的结果,放入到 useAsyncEffect
中不就可以了?
实现最终类似如下:
1function useAsyncEffect(effect: () => Promise<void | (() => void)>, dependencies?: any[]) { 2 return useEffect(() => { 3 const cleanupPromise = effect() 4 return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) } 5 }, dependencies) 6}
这种做法在github上也有讨论,上面有个大神的说法我表示很赞同:
他认为这种延迟清除机制是不对的,应该是一种取消机制。否则,在钩子已经被取消之后,回调函数仍然有机会对外部状态产生影响。他的实现和例子我也贴一下,跟 useAsyncEffect
其实思路是一样的,如下:
实现:
function useAsyncEffect(effect: (isCanceled: () => boolean) => Promise<void>, dependencies?: any[]) {
return useEffect(() => {
let canceled = false;
effect(() => canceled);
return () => { canceled = true; }
}, dependencies)
}
Demo:
1useAsyncEffect(async (isCanceled) => { 2 const result = await doSomeAsyncStuff(stuffId); 3 if (!isCanceled()) { 4 // TODO: Still OK to do some effect, useEffect hasn't been canceled yet. 5 } 6}, [stuffId]);
其实归根结底,我们的清除机制不应该依赖于异步函数,否则很容易出现难以定位的 bug。
由于 useEffect
是在函数式组件中承担执行副作用操作的职责,它的返回值的执行操作应该是可以预期的,而不能是一个异步函数,所以不支持回调函数 async...await
的写法。
我们可以将 async...await
的逻辑封装在 useEffect
回调函数的内部,这就是 ahooks useAsyncEffect
的实现思路,而且它的范围更加广,它支持的是所有的异步函数,包括 generator function
。
最近更新时间:2024-08-10