• 如何让 useEffect 支持 async/await?


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

    看报错,我们知道 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 的方式),然后执行该函数。

    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. }

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

    他认为这种延迟清除机制是不对的,应该是一种取消机制。否则,在钩子已经被取消之后,回调函数仍然有机会对外部状态产生影响。跟 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 回调函数的内部,这就是 hooks useAsyncEffect 的实现思路,而且它的范围更加广,它支持的是所有的异步函数,包括 generator function

  • 相关阅读:
    学习太极创客 — MQTT 第二章(八)ESP8266 MQTT 用户密码认证
    KVB交易平台:苹果股价大涨超7%,iPhone超级周期来临?
    安装gymnasium[box2d]的问题
    【LeetCode】795.区间子数组个数
    暑期JAVA学习(42.1)TCP通信——使用线程池优化
    9.axios 拦截器的使用,对config 拦截器做的封装
    Spring Boot问题汇总
    数据结构学习笔记——基数排序和排序算法总结
    浅谈JMM和并发三大特性(volatile、MESI、内存屏障)
    LeetCode每日一题(2380. Time Needed to Rearrange a Binary String)
  • 原文地址:https://blog.csdn.net/zz130428/article/details/128213900