useEffect 怎麼支持 async---await

本文是深入淺出 ahooks 源碼系列文章的第六篇,這個系列的目標主要有以下幾點:

注:本系列對 ahooks 的源碼解析是基於 v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見 詳情 [1]。

背景

大家在使用 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[2] 中有討論,上面有個大神的說法我表示很贊同:

他認爲這種延遲清除機制是不對的,應該是一種取消機制。否則,在鉤子已經被取消之後,回調函數仍然有機會對外部狀態產生影響。他的實現和例子我也貼一下,跟 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

系列文章:

參考

參考資料

[1]

詳情: https://github.com/GpingFeng/hooks

[2]

issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30551

[3]

大家都能看得懂的源碼(一)ahooks 整體架構篇: https://juejin.cn/post/7105396478268407815

[4]

如何使用插件化機制優雅的封裝你的請求 hook : https://juejin.cn/post/7105733829972721677

[5]

ahooks 是怎麼解決 React 的閉包問題的?: https://juejin.cn/post/7106061970184339464

[6]

ahooks 是怎麼解決用戶多次提交問題?: https://juejin.cn/post/7106461530232717326

[7]

ahooks 中那些控制 “時機” 的 hook 都是怎麼實現的?: https://juejin.cn/post/7107189225509879838

[8]

React useEffect 不支持 async function 你知道嗎?: https://zhuanlan.zhihu.com/p/425129987

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iGtIW7Sz9IiX-c7oBWdFEg