[前端] useEffect支持async及await使用方式

1760 0
黑夜隐士 2022-10-21 15:31:00 | 显示全部楼层 |阅读模式
目录

    引言背景React 为什么要这么做?useEffect 怎么支持 async...await...自定义 hooks还可以支持 useEffect 的清除机制么?总结与思考


引言

本文是深入浅出 ahooks 源码系列文章的第六篇,这个系列的目标主要有以下几点:
    加深对 React hooks 的理解。学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。
注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情。

背景

大家在使用 useEffect 的时候,假如回调函数中使用 async...await... 的时候,会报错如下。


看报错,我们知道 effect function 应该返回一个销毁函数(effect:是指return返回的cleanup函数),如果 useEffect 第一个参数传入 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错。

React 为什么要这么做?

useEffect 作为 Hooks 中一个很重要的 Hooks,可以让你在函数组件中执行副作用操作。 它能够完成之前 Class Component 中的生命周期的职责。它返回的函数的执行时机如下:
    首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用。卸载阶段也会执行清除操作。
不管是哪个,我们都不希望这个返回值是异步的,这样我们无法预知代码的执行情况,很容易出现难以定位的 Bug。所以 React 就直接限制了不能 useEffect 回调函数中不能支持 async...await...

useEffect 怎么支持 async...await...

竟然 useEffect 的回调函数不能使用 async...await,那我直接在它内部使用。
做法一:创建一个异步函数(async...await 的方式),然后执行该函数。
  1. useEffect(() => {
  2.   const asyncFun = async () => {
  3.     setPass(await mockCheck());
  4.   };
  5.   asyncFun();
  6. }, []);
复制代码
做法二:也可以使用 IIFE,如下所示:
  1. useEffect(() => {
  2.   (async () => {
  3.     setPass(await mockCheck());
  4.   })();
  5. }, []);
复制代码
自定义 hooks

既然知道了怎么解决,我们完全可以将其封装成一个 hook,让使用更加的优雅。我们来看下 ahooks 的 useAsyncEffect,它支持所有的异步写法,包括 generator function。
思路跟上面一样,入参跟 useEffect 一样,一个回调函数(不过这个回调函数支持异步),另外一个依赖项 deps。内部还是 useEffect,将异步的逻辑放入到它的回调函数里面。
  1. function 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 就是用来取消当前正在执行的一个标识符。

还可以支持 useEffect 的清除机制么?

可以看到上面的 useAsyncEffect,内部的 useEffect 返回函数只返回了如下:
  1. return () => {
  2.   // 当前 effect 已经被清理
  3.   cancelled = true;
  4. };
复制代码
这说明,你通过 useAsyncEffect 没有 useEffect 返回函数中执行清除副作用的功能。
你可能会觉得,我们将 effect(useAsyncEffect 的回调函数)的结果,放入到 useAsyncEffect 中不就可以了?
实现最终类似如下:
  1. function useAsyncEffect(effect: () => Promise<void | (() => void)>, dependencies?: any[]) {
  2.   return useEffect(() => {
  3.     const cleanupPromise = effect()
  4.     return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) }
  5.   }, dependencies)
  6. }
复制代码
这种做法在这个 issue 中有讨论,上面有个大神的说法我表示很赞同:


他认为这种延迟清除机制是不对的,应该是一种取消机制。否则,在钩子已经被取消之后,回调函数依旧有机会对外部状态产生影响。他的实现和例子我也贴一下,跟 useAsyncEffect 其实思路是一样的,如下:
实现:
  1. function useAsyncEffect(effect: (isCanceled: () => boolean) => Promise<void>, dependencies?: any[]) {
  2.   return useEffect(() => {
  3.     let canceled = false;
  4.     effect(() => canceled);
  5.     return () => { canceled = true; }
  6.   }, dependencies)
  7. }
复制代码
Demo:
  1. useAsyncEffect(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。
参考    React useEffect 不支持 async function
以上就是useEffect支持async及await使用方式的详细内容,更多关于useEffect支持async及await的资料请关注中国红客联盟其它相关文章!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行