節(jié)流與防抖都是通過延遲執(zhí)行,減少調(diào)用次數(shù)燎孟,來優(yōu)化頻繁調(diào)用函數(shù)時的性能禽作。不同的是,對于一段時間內(nèi)的頻繁調(diào)用揩页,防抖是 延遲執(zhí)行 一次調(diào)用旷偿,節(jié)流是 延遲定時 多次調(diào)用。
前言
不知道有多少人爆侣,簡單的寫了防抖萍程、節(jié)流函數(shù),然后遇到在 react hook 里失效的情況兔仰。
失效的原因: 每次 render 時茫负,內(nèi)部函數(shù)會重新生成并綁定到組件上去。
解決方案:也很簡單乎赴,使用 useCallback 忍法,依賴傳入空數(shù)組,保證 useCallback 永遠(yuǎn)返回同一個函數(shù)无虚。
上面呢缔赠,算是這個文章的一個契機吧。
關(guān)于手寫防抖和節(jié)流的思路友题,個人認(rèn)為關(guān)鍵在于都是對 閉包 和 高階函數(shù) 的應(yīng)用嗤堰,以這個為切入點去思考,手寫的時候就不會腦子一片空白了度宦。
防抖(debounce)
觸發(fā)事件后在 n 秒內(nèi)函數(shù)只能執(zhí)行一次踢匣,如果在 n 秒內(nèi)又觸發(fā)了事件,則會重新計算函數(shù)執(zhí)行時間戈抄。
初步
import { useCallback } from 'react';
/**
* 防抖hook
* @param func 需要執(zhí)行的函數(shù)
* @param wait 延遲時間
*/
export function useDebounce<A extends Array<any>, R = void>(
func: (..._args: A) => R,
wait: number,
) {
let timeOut: null | NodeJS.Timeout = null;
function debounced(..._args: A) {
if (timeOut) {
clearTimeout(timeOut);
timeOut = null;
}
timeOut = setTimeout(() => {
fn.apply(null, _args);
}, wait);
}
return useCallback(debounced, []);
}
這可以用离唬,但并不夠好。想要進(jìn)階更高級的工程師划鸽,就需要將問題再想深一層输莺,考慮到更復(fù)雜的情況,從而自身得到成長裸诽。
進(jìn)階版
- 首先想到的是要返回一個 Promise 嫂用,用來傳遞返回值。
- 其次考慮到異步的情況丈冬,增加 async嘱函。
- 最后是防抖化之后是否可以立即執(zhí)行和取消,所以增加2個新函數(shù)埂蕊。
import { useCallback } from 'react';
/**
* 防抖hook
* @param func 需要執(zhí)行的函數(shù)
* @param wait 延遲時間
*/
export function useDebounce<A extends Array<any>, R = void>(
func: (..._args: A) => R,
wait: number,
) {
let timeOut: null | NodeJS.Timeout = null;
let args: A;
function debounce(..._args: A) {
args = _args;
if (timeOut) {
clearTimeout(timeOut);
timeOut = null;
}
return new Promise<R>((resolve, reject) => {
timeOut = setTimeout(async () => {
try {
const result = await func.apply(null, args);
resolve(result);
} catch (e) {
reject(e);
}
}, wait);
});
}
//取消
function cancel() {
if (!timeOut) return;
clearTimeout(timeOut);
timeOut = null;
}
//立即執(zhí)行
function flush() {
cancel();
return func.apply(null, args);
}
debounce.flush = flush;
debounce.cancel = flush;
return useCallback(debounce, []);
}
關(guān)于防抖函數(shù)還有功能更豐富的版本往弓,可以看下 lodash 的 debounce 函數(shù)
節(jié)流(throttle)
連續(xù)觸發(fā)事件但是在 n 秒中只執(zhí)行一次函數(shù)
節(jié)流函數(shù)的2種思路
時間戳:通過記錄上次執(zhí)行的時間戳疏唾, 和當(dāng)前時間戳比較來判斷是否已到執(zhí)行時間 ,如果是則執(zhí)行函似,并更新上次執(zhí)行的時間戳槐脏。(問題在于:事件停止觸發(fā)時無法執(zhí)行函數(shù))
定時器:如果已經(jīng)存在定時器,則不執(zhí)行方法缴淋,直到定時器觸發(fā)后被清除准给,然后重新設(shè)置定時器。(問題在于:事件停止觸發(fā)后必然會再執(zhí)行函數(shù))
整合版
把兩個整合一下重抖,根據(jù)場景、需求等來決定祖灰,最后是否需要事件停止觸發(fā)后定時器執(zhí)行函數(shù)钟沛。
/**
* 節(jié)流hook
* @param func 需要執(zhí)行的函數(shù)
* @param wait 延遲時間
* @param isTimer 是否開啟定時器響應(yīng)事件結(jié)束后的回調(diào)
*/
export function useThrottle<A extends Array<any>, R = void>(
func: (..._args: A) => R,
wait: number,
isTimer: boolean = false,
) {
let timeOut: null | NodeJS.Timeout = null;
let args: A;
let agoTimestamp: number;
function throttle(..._args: A) {
args = _args;
if (!agoTimestamp) agoTimestamp = +new Date();
if (timeOut) {
clearTimeout(timeOut);
timeOut = null;
}
return new Promise<R>((resolve, reject) => {
if (+new Date() - agoTimestamp >= wait) {
try {
const result = func.apply(null, args);
resolve(result);
agoTimestamp = +new Date();
} catch (e) {
reject(e);
}
} else if (isTimer) {
timeOut = setTimeout(async () => {
try {
const result = await func.apply(null, args);
resolve(result);
agoTimestamp = +new Date();
} catch (e) {
reject(e);
}
}, agoTimestamp + wait - +new Date());
}
});
}
//取消
function cancel() {
if (!timeOut) return;
clearTimeout(timeOut);
timeOut = null;
}
//立即執(zhí)行
function flush() {
cancel();
return func.apply(null, args);
}
throttle.flush = flush;
throttle.cancel = flush;
return useCallback(throttle, []);
}
最后
有個地方有人可能有疑問,為什么沒去用 useRef 去保存 timeOut 呢局扶?
有人可能會認(rèn)為這會有問題:因為每次組件重新渲染恨统,都會執(zhí)行一遍所有的 hooks,這樣 useDebounce 高階函數(shù)里面的 timeOut 就不能起到緩存的作用(在 useDebounce 里 console.log(timeOut)三妈,每次 render 時都打印出 null)畜埋。所以 timeOut 不可靠,防抖的核心就被破壞了畴蒲。
但是呢悠鞍,如果你在里面的函數(shù) debounce 里 console.log(timeOut) 會發(fā)現(xiàn),打印出來的模燥,就是之前的 timeOut 咖祭,所以是沒問題的。
最后蔫骂,寫的過程中么翰,ts 才是我真正花費時間思考的地方。完成后辽旋,有點微妙的滿足感浩嫌。