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

来源:https://www.cnblogs.com/gopal/archive/2022/08/15/16587929.html
-Advertisement-
Play Games

本文是深入淺出 ahooks 源碼系列文章的第六篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本文已收錄到個人博客中,歡迎關註~ 背景 大家在使用 useEffect 的時候,假如回調函數中使用 async...await... 的時候,會報錯如下。 看報錯, ...


本文是深入淺出 ahooks 源碼系列文章的第六篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,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

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當一條SQL執行較慢,需要分析性能瓶頸,到底慢在哪? 我們一般會使用Explain查看其執行計劃,從執行計劃中得知這條SQL有沒有使用索引?使用了哪個索引? 但是執行計劃顯示內容不夠詳細,如果顯示用到了某個索引,查詢依然很慢,我們就無法得知具體是哪一步比較耗時? 好在MySQL提供一個SQL性能分析... ...
  • To digitally transform the business, AI must be real-time. For AI to be real-time, we need real-time analytics.[1] Hybrid transaction/analytical proce ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 my2sql工具之快速入門 1.什麼是my2sql 2.如何快速部署my2sql工具 3.如何使用my2sql工具 3.1使用my2sql工具解析bin ...
  • SQL 作為關係型資料庫的標準語言,是 IT 從業人員必不可少的技能之一。SQL 本身並不難學,編寫查詢語句也很容易,但是想要編寫出能夠高效運行的查詢語句卻有一定的難度。 查詢優化是一個複雜的工程,涉及從硬體到參數配置、不同資料庫的解析器、優化器實現、SQL 語句的執行順序、索引以及統計信息的採集等... ...
  • 概述 這是一個自定義色盤,根據點,直線和圓的幾何學加上hsv顏色模型完成 技術點 幾何: 圓的標準方程式:(x-a)²+(y-b)²=r² 直線一般方程式: 已知直線上的兩點P1(X1,Y1) P2(X2,Y2), P1 P2兩點不重合。 AX+BY+C=0 A = Y2 - Y1 B = X1 - ...
  • Android 知識體系 一、平臺架構 Google Android 平臺架構 Google Android 架構 Android 是一個針對多種不同設備類型打造的開放源代碼軟體堆棧。Android 的主要目的是為運營商、OEM 和開發者打造一個開放的軟體平臺,使他們能夠將創新理念變為現實,並推出能 ...
  • 原文:Kotlin學習快速入門(10)—— 重載運算符使用 - Stars-One的雜貨小窩 Kotlin中提供了基礎的運算符,但是只是針對基礎的數據類型,如Int,Double等 如果我們想讓兩個對象可以相加的功能,這個時候可以使用重載運算符的功能來實現 介紹 首先,先介紹下什麼是運算符,如以下代 ...
  • 區別一:存儲數據大小不同 1.cookie的存儲數據大小在不能超過4kb,每個頁面最多存儲20個cookie 2.localStorage能達到10mb,sessionStorage能達到5mb,雖然容量比cookie大,但是localStorage是同步執行,太大會影響渲染進度 區別二:相容性 1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...