关于前端:如何让-useEffect-支持-asyncawait

8次阅读

共计 3270 个字符,预计需要花费 9 分钟才能阅读完成。

本文是深入浅出 ahooks 源码系列文章的第六篇,该系列已整顿成文档 - 地址。感觉还不错,给个 关注 反对一下哈,Thanks。

本文已收录到集体博客中,欢送关注~

背景

大家在应用 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 的形式),而后执行该函数。

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

做法二:也能够应用 IIFE,如下所示:

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

自定义 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)
}

这种做法在这个 issue 中有探讨,下面有个大神的说法我示意很同意:

他认为这种 提早革除机制 是不对的,应该是一种 勾销机制。否则,在钩子曾经被勾销之后,回调函数依然有机会对外部状态产生影响。他的实现和例子我也贴一下,跟 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 回调函数的外部,这就是 ahooks useAsyncEffect 的实现思路,而且它的范畴更加广,它反对的是所有的异步函数,包含 generator function

正文完
 0