如何让 useEffect 支持 async/await?

简介: 如何让 useEffect 支持 async/await?

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

b8f31d4ddc9a4eac920813f1197d5e4b.png

看报错,我们知道 effect function 应该返回一个销毁函数(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 的方式),然后执行该函数。

useEffect(() => {
  const asyncFun = async () => {
    setPass(await mockCheck());
  };
  asyncFun();
}, []);

做法二:也可以使用 IIFE,如下所示:

1. useEffect(() => {
2.   (async () => {
3. setPass(await mockCheck());
4.   })();
5. }, []);

自定义 hooks


既然知道了怎么解决,我们完全可以将其封装成一个 hook,让使用更加的优雅。我们来看下 ahooks 的 useAsyncEffect,它支持所有的异步写法,包括 generator function

思路跟上面一样,入参跟 useEffect 一样,一个回调函数(不过这个回调函数支持异步),另外一个依赖项 deps。内部还是 useEffect,将异步的逻辑放入到它的回调函数里面。

function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  // 依赖项
  deps?: DependencyList,
) {
  // 判断是 AsyncGenerator
  function isAsyncGenerator(
    val: AsyncGenerator<void, void, void> | Promise<void>,
  ): val is AsyncGenerator<void, void, void> {
    // Symbol.asyncIterator: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator
    // Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。
    return isFunction(val[Symbol.asyncIterator]);
  }
  useEffect(() => {
    const e = effect();
    // 这个标识可以通过 yield 语句可以增加一些检查点
    // 如果发现当前 effect 已经被清理,会停止继续往下执行。
    let cancelled = false;
    // 执行函数
    async function execute() {
      // 如果是 Generator 异步函数,则通过 next() 的方式全部执行
      if (isAsyncGenerator(e)) {
        while (true) {
          const result = await e.next();
          // Generate function 全部执行完成
          // 或者当前的 effect 已经被清理
          if (result.done || cancelled) {
            break;
          }
        }
      } else {
        await e;
      }
    }
    execute();
    return () => {
      // 当前 effect 已经被清理
      cancelled = true;
    };
  }, deps);
}

async...await 我们之前已经提到了,重点看看实现中变量 cancelled 的实现的功能。 它的作用是中断执行

通过 yield 语句可以增加一些检查点,如果发现当前 effect 已经被清理,会停止继续往下执行。

试想一下,有一个场景,用户频繁的操作,可能现在这一轮操作 a 执行还没完成,就已经开始开始下一轮操作 b。这个时候,操作 a 的逻辑已经失去了作用了,那么我们就可以停止往后执行,直接进入下一轮操作 b 的逻辑执行。这个 cancelled 就是用来取消当前正在执行的一个标识符。

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


可以看到上面的 useAsyncEffect,内部的 useEffect 返回函数只返回了如下:

return () => {
  // 当前 effect 已经被清理
  cancelled = true;
};

这说明,你通过 useAsyncEffect 没有 useEffect 返回函数中执行清除副作用的功能

你可能会觉得,我们将 effect(useAsyncEffect 的回调函数)的结果,放入到 useAsyncEffect 中不就可以了?

实现最终类似如下:

function useAsyncEffect(effect: () => Promise<void | (() => void)>, dependencies?: any[]) {
  return useEffect(() => {
    const cleanupPromise = effect()
    return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) }
  }, dependencies)
}

这种做法在github上也有讨论,上面有个大神的说法:

他认为这种延迟清除机制是不对的,应该是一种取消机制。否则,在钩子已经被取消之后,回调函数仍然有机会对外部状态产生影响。跟 useAsyncEffect 其实思路是一样的,如下:

实现:

function useAsyncEffect(effect: (isCanceled: () => boolean) => Promise<void>, dependencies?: any[]) {
  return useEffect(() => {
    let canceled = false;
    effect(() => canceled);
    return () => { canceled = true; }
  }, dependencies)
}

Demo:

useAsyncEffect(async (isCanceled) => {
  const result = await doSomeAsyncStuff(stuffId);
  if (!isCanceled()) {
    // TODO: Still OK to do some effect, useEffect hasn't been canceled yet.
  }
}, [stuffId]);

其实归根结底,我们的清除机制不应该依赖于异步函数,否则很容易出现难以定位的 bug

总结与思考

由于 useEffect 是在函数式组件中承担执行副作用操作的职责,它的返回值的执行操作应该是可以预期的,而不能是一个异步函数,所以不支持回调函数 async...await 的写法。

可以将 async...await 的逻辑封装在 useEffect 回调函数的内部,这就是 hooks useAsyncEffect 的实现思路,而且它的范围更加广,它支持的是所有的异步函数,包括 generator function

相关文章
|
4月前
|
自然语言处理 前端开发 JavaScript
用 通义灵码 一键生成“水波纹按钮”,连 CSS 动画都不用自己写了!
通义灵码是一款智能编程辅助工具,它可以根据自然语言指令自动生成高质量的代码。例如,只需输入“生成一个按钮,点击时带水波纹动画,模拟 Material Ripple 效果”,它就能生成具备完整交互逻辑、CSS 动画和良好性能的按钮组件。不仅如此,它还支持拓展功能,如长按触发提示、添加图标等,并能自动优化样式适配不同场景。通过通义灵码,开发者可以大幅提升效率,专注于创意实现,而不必纠结于繁琐的代码细节。
|
存储 运维 开发工具
警惕日志采集失败的 6 大经典雷区:从本地管理反模式到 LoongCollector 标准实践
本文探讨了日志管理中的常见反模式及其潜在问题,强调科学的日志管理策略对系统可观测性的重要性。文中分析了6种反模式:copy truncate轮转导致的日志丢失或重复、NAS/OSS存储引发的采集不一致、多进程写入造成的日志混乱、创建文件空洞释放空间的风险、频繁覆盖写带来的数据完整性问题,以及使用vim编辑日志文件导致的重复采集。针对这些问题,文章提供了最佳实践建议,如使用create模式轮转日志、本地磁盘存储、单线程追加写入等方法,以降低日志采集风险,提升系统可靠性。最后总结指出,遵循这些实践可显著提高故障排查效率和系统性能。
979 21
|
JavaScript 前端开发 API
探索组合式API的革新与优势
探索组合式API的革新与优势
202 1
|
前端开发 API 定位技术
前端获取地区的天气状况
前端获取地区的天气状况
498 0
|
前端开发 索引
解决React报错Encountered two children with the same key
解决React报错Encountered two children with the same key
|
机器学习/深度学习 搜索推荐 算法
个性化音色定制:FunAudioLLM 的用户偏好学习机制
【8月更文第28天】随着语音合成技术的发展,越来越多的应用程序允许用户定制自己喜爱的声音类型。FunAudioLLM 是一个虚构的语音合成框架,它利用机器学习算法来适应用户的个人偏好,从而生成更加个性化的音色。本文将介绍 FunAudioLLM 如何通过用户偏好学习机制来调整和优化声音输出,并提供一些示例代码。
457 0
|
JavaScript 前端开发
如何解决 Critical dependency: the request of a dependency is an expression ?
如何解决 Critical dependency: the request of a dependency is an expression ?
364 0
|
人工智能 搜索推荐 前端开发
人工智能(AI)在前端设计中应用
人工智能(AI)在前端设计中应用
1044 0
|
存储 SQL 关系型数据库
MySQL 查询语句大全
MySQL 查询语句大全
332 2
|
Java Spring
springboot异常处理之404
springboot异常处理之404
584 1
springboot异常处理之404