本篇通譯自 https://blog.devgenius.io/how-to-better-poll-apis-in-react-312bddc604a4
作者: Zachary Lee
替代 setInterval,間隔調(diào)用異步方法的更好解決方案

在 Web 開發(fā)中,我們可能需要不斷地輪詢后端 API 以獲取頁(yè)面上要更新的最新數(shù)據(jù)。雖然 WebSocket 是更好的選擇,但在某些情況下輪詢也可以。
那么如何在 React 中做到這一點(diǎn)呢?
setInterval
我們可以使用 setInterval 連續(xù)執(zhí)行 async 方法,這可能是最簡(jiǎn)單的解決方案。
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
useEffect(() => {
setInterval(updateState, 3000);
}, [updateState]);
return <main>{`Your origin is: ${origin}`}</main>;
};
但是這個(gè)解決方案有一些問(wèn)題。首先setInterval是不準(zhǔn)確的, 二是粒度不易控制,造成浪費(fèi)。例如,對(duì)于一個(gè)響應(yīng)時(shí)間較長(zhǎng)的 API 請(qǐng)求,上一次響應(yīng)的內(nèi)容在頁(yè)面上還沒有更新,下一個(gè)請(qǐng)求會(huì)重新發(fā)送。
這并不理想。
setTimeout + async…await
我們可以使用setTimeoutand async...await來(lái)實(shí)現(xiàn)一個(gè)自定義鉤子:
const useIntervalAsync = (fn: () => Promise<unknown>, ms: number) => {
const timeout = useRef<number>();
const run = useCallback(async () => {
await fn();
timeout.current = window.setTimeout(run, ms);
}, [fn, ms]);
useEffect(() => {
run();
return () => {
window.clearTimeout(timeout.current);
};
}, [run]);
};
接下來(lái),它是這樣使用的:
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
useIntervalAsync(updateState, 3000);
return <main>{`Your origin is: ${origin}`}</main>;
};
此解決方案使用async...await和setTimeout來(lái)確保異步任務(wù)在上一個(gè)異步任務(wù)完成后再執(zhí)行
但是異步任務(wù)總是很棘手。想象一個(gè)案例:如果使用這個(gè)鉤子的組件在等待異步響應(yīng)時(shí)被卸載,那么這個(gè)定時(shí)任務(wù)將一直在后臺(tái)運(yùn)行。這是一個(gè)嚴(yán)重的錯(cuò)誤,我們可以通過(guò)記錄掛載狀態(tài)來(lái)避免這種情況。
const useIntervalAsync = (fn: () => Promise<unknown>, ms: number) => {
const timeout = useRef<number>();
const mountedRef = useRef(false);
const run = useCallback(async () => {
await fn();
if (mountedRef.current) {
timeout.current = window.setTimeout(run, ms);
}
}, [fn, ms]);
useEffect(() => {
mountedRef.current = true;
run();
return () => {
mountedRef.current = false;
window.clearTimeout(timeout.current);
};
}, [run]);
};
可以通過(guò)記錄mountedRef. 很簡(jiǎn)單,對(duì),但其實(shí)很實(shí)用,當(dāng)然你也可以把mounted狀態(tài)做成自定義hook,方便復(fù)用。例如useMountedState下面:
import { useCallback, useEffect, useRef } from 'react';
const useMountedState = () => {
const mountedRef = useRef(false);
const getState = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
return getState;
};
export default useMountedState;
復(fù)雜案例
在一些比較復(fù)雜的情況下,我們可能需要主動(dòng)更新頁(yè)面信息。例如,在一些交互之后,我希望updateState立即調(diào)用以更新頁(yè)面上的最新數(shù)據(jù)。
當(dāng)然我可以updateState直接調(diào)用,但是下一個(gè)定時(shí)任務(wù)可能會(huì)執(zhí)行的很快,很浪費(fèi)。所以我們可以添加一些特性來(lái)useIntervalAsync讓它支持刷新。
import { useCallback, useEffect, useRef } from 'react';
const useIntervalAsync = <R = unknown>(fn: () => Promise<R>, ms: number) => {
const runningCount = useRef(0);
const timeout = useRef<number>();
const mountedRef = useRef(false);
const next = useCallback(
(handler: TimerHandler) => {
if (mountedRef.current && runningCount.current === 0) {
timeout.current = window.setTimeout(handler, ms);
}
},
[ms],
);
const run = useCallback(async () => {
runningCount.current += 1;
const result = await fn();
runningCount.current -= 1;
next(run);
return result;
}, [fn, next]);
useEffect(() => {
mountedRef.current = true;
run();
return () => {
mountedRef.current = false;
window.clearTimeout(timeout.current);
};
}, [run]);
const flush = useCallback(() => {
window.clearTimeout(timeout.current);
return run();
}, [run]);
return flush;
};
export default useIntervalAsync;
可以看到我們已經(jīng)添加了flush方法。它的內(nèi)部邏輯是取消下一個(gè)定時(shí)任務(wù),run直接執(zhí)行該方法。
但是我們添加了一個(gè)runningCount,這是為了什么?
想象一個(gè)案例:當(dāng)鉤子在內(nèi)部run以正常的邏輯間隔執(zhí)行函數(shù)時(shí),而異步響應(yīng)正在等待解決,flush被外部調(diào)用以期望立即執(zhí)行,然后run將再次執(zhí)行。這是因?yàn)樽詈笠粋€(gè)run還沒有解決,最新的計(jì)劃任務(wù)還沒有創(chuàng)建,所以不能取消。
也就是說(shuō),run此時(shí)有兩個(gè)函數(shù)正在執(zhí)行,雖然這并不關(guān)鍵,但是如果我們?nèi)匀皇褂们懊娴倪壿嫞敲催@兩個(gè)run在解決后會(huì)創(chuàng)建兩個(gè)定時(shí)任務(wù)。這會(huì)導(dǎo)致更大的浪費(fèi)。
所以我們可以使用runningCount來(lái)記錄當(dāng)前的執(zhí)行次數(shù),并在函數(shù)中保證只有在是下一個(gè)任務(wù)時(shí)才創(chuàng)建新的定時(shí)任務(wù)。runningCount 0
另外,通過(guò) TypeScript 的泛型,我們可以很方便的包裝原有的函數(shù)以適應(yīng)更多的情況。一個(gè)簡(jiǎn)單的例子:
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
const update = useIntervalAsync(updateState, 3000);
return (
<main>
<div>{`Your origin is: ${origin}`}</div>
<button onClick={update}>update</button>
</main>
);
};
本文由mdnice多平臺(tái)發(fā)布