本文是深入淺出 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,
參考
- React useEffect 不支持 async function 你知道嗎?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/501941.html
標籤:其他
上一篇:根據兩點經緯度計算出航向
