如何讓 useEffect 支援 async...await?

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 17 天,點選檢視活動詳情

本文是深入淺出 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 的方式),然後執行該函式。

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

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

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

自定義 hooks

既然知道了怎麼解決,我們完全可以將其封裝成一個 hook,讓使用更加的優雅。我們來看下 ahooks 的 useAsyncEffect,它支援所有的非同步寫法,包括 generator function。

思路跟上面一樣,入參跟 useEffect 一樣,一個回撥函式(不過這個回撥函式支援非同步),另外一個依賴項 deps。內部還是 useEffect,將非同步的邏輯放入到它的回撥函式裡面。

js 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: http://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 返回函式只返回瞭如下:

js return () => { // 當前 effect 已經被清理 cancelled = true; };

這說明,你通過 useAsyncEffect 沒有 useEffect 返回函式中執行清除副作用的功能

你可能會覺得,我們將 effect(useAsyncEffect 的回撥函式)的結果,放入到 useAsyncEffect 中不就可以了?

實現最終類似如下:

js 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: js 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

系列文章: - 大家都能看得懂的原始碼(一)ahooks 整體架構篇 - 如何使用外掛化機制優雅的封裝你的請求hook - ahooks 是怎麼解決 React 的閉包問題的? - ahooks 是怎麼解決使用者多次提交問題? - ahooks 中那些控制“時機”的hook都是怎麼實現的?

參考