手寫防抖、節(jié)流 hook(ts版)

節(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 永遠返回同一個函數(shù)。

上面呢,算是這個文章的一個契機吧。

關(guān)于手寫防抖和節(jié)流的思路,個人認為關(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, []);
}

這可以用,但并不夠好。想要進階更高級的工程師,就需要將問題再想深一層,考慮到更復(fù)雜的情況,從而自身得到成長。

進階版
  1. 首先想到的是要返回一個 Promise ,用來傳遞返回值。
  2. 其次考慮到異步的情況,增加 async。
  3. 最后是防抖化之后是否可以立即執(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ù)還有功能更豐富的版本,可以看下 lodashdebounce 函數(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 呢?

有人可能會認為這會有問題:因為每次組件重新渲染,都會執(zhí)行一遍所有的 hooks,這樣 useDebounce 高階函數(shù)里面的 timeOut 就不能起到緩存的作用(在 useDebounce 里 console.log(timeOut),每次 render 時都打印出 null)。所以 timeOut 不可靠,防抖的核心就被破壞了。

但是呢,如果你在里面的函數(shù) debounce 里 console.log(timeOut) 會發(fā)現(xiàn),打印出來的,就是之前的 timeOut ,所以是沒問題的。

最后,寫的過程中,ts 才是我真正花費時間思考的地方。完成后,有點微妙的滿足感。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本篇文章主要介紹防抖和節(jié)流的原理,以及它們的區(qū)別。 防抖與節(jié)流的問題總是會在面試中出現(xiàn)(然而我并沒有遇到),如果你...
    黑色瓶子閱讀 1,244評論 0 3
  • 1. 認識防抖和節(jié)流 1.1. 對防抖和節(jié)流的認識 防抖和節(jié)流的概念其實最早并不是出現(xiàn)在軟件工程中,防抖是出現(xiàn)在電...
    AShuiCoder閱讀 1,187評論 0 10
  • 節(jié)流(throttle)與防抖(debounce) 場景 因頻繁執(zhí)行DOM操作,資源加載等行為,導(dǎo)致UI停頓甚至瀏...
    學(xué)編程的小屁孩閱讀 556評論 0 0
  • 有些瀏覽器事件可以在短時間內(nèi)快速觸發(fā)多次,比如調(diào)整窗口大小或向下滾動頁面。例如,監(jiān)聽頁面窗口滾動事件,并且用戶持續(xù)...
    劉其瑞閱讀 2,704評論 0 1
  • 在上周的開發(fā)中,又遇到點擊保存多次請求數(shù)據(jù)重復(fù)的問題,所以下來學(xué)習(xí)了一下js的防抖和節(jié)流。通過學(xué)習(xí)了解到,在進行窗...
    any_5637閱讀 435評論 0 2

友情鏈接更多精彩內(nèi)容