防抖與節流:實踐與勘誤

防抖與節流:實踐與勘誤

原文鏈接

前言

一般對於監聽某些密集型鍵盤、鼠標、手勢事件需要和後端請求交互、修改 dom 的,防抖、節流就很有必要了。

防抖

使用場景

  • 關鍵字遠程搜索下拉框
  • resize

對於這類操作,一般希望拿到用戶最終輸入的關鍵字、確定的拖拽大小,然後與服務器交互。
而中間態的值,並不關心,爲了減輕服務器壓力,避免服務器資源浪費,這時就需要防抖了。

案例

  • 輸入框防抖
// 記錄時間
let last = new Date().getTime();
  //模擬一段ajax請求
function ajax(content) {
  const d = new Date().getTime();
  const span = d - last;
  console.log(`${content} 間隔 ${span}ms`);
  last = d;
}

const noActionInput = document.getElementById('noAction');

noActionInput.addEventListener('keyup', function(e) {
  ajax(e.target.value);
});
  • 未防抖
    在這裏插入圖片描述

可以看到爲太多的中間態發送太多的請求。

/**
* 一般利用閉包存儲和私有化定時器 `timer`
* 在 `delay` 時間內再次調用則清除未執行的定時器
* 重新定時器
* @param fn
* @param delay
* @returns {Function}
*/
function debounce(fn, delay) {
  let timer = null;
  return function() {
    // 中間態一律清除掉
    timer && clearTimeout(timer);
    // 只需要最終的狀態,執行
    timer = setTimeout(() => fn.apply(this, arguments), delay);
  };
}
    
const debounceInput = document.getElementById('debounce');

let debounceAjax = debounce(ajax, 100);

debounceInput.addEventListener('keyup', function(e) {
  debounceAjax(e.target.value);
});
  • 防抖後

在這裏插入圖片描述

可以發現:

  1. 如果輸入的很慢,差不多每 delay 100ms 執行一次;
  2. 如果輸入的很快,說明用戶在連續性輸入,則會等到用戶差不輸入完慢下來了在執行回調。

節流

使用場景

  • 滑動滾動條
  • 射擊類遊戲發射子彈
  • 水龍頭的水流速

對於某些連續性的事件,爲了表現平滑過渡,這時的中間態我們也需要關心的。
但減弱密集型事件的頻率依舊是性能優化的殺器。

勘誤

非常常見的兩種錯誤寫法,太流行了,忍不住出來勘誤。

// 時間戳版
function throttleError1(fn, delay) {
  let lastTime = 0;
  return function () {
    const now = new Date().getTime();
    const space = now - lastTime; // 時間間隔,首次會是很大一個值
    if (space > delay) {
      lastTime = now;
      fn.apply(this, arguments);
    }
  };
}}

// 定時器版
function throttleError2(fn, delay) {
  let timer;
  return function () {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        fn.apply(this, arguments);
      }, delay);
    }
  };
}

這兩個版本都有的問題,先假設 delay=100ms,假設定時器都是按時執行的。

  • 時間戳版
  1. 由於首次 now - lastTime === now 該值很大,首次 0ms 立即執行,用戶接在 0-100ms 內執行的交互均無效,假如用戶停留在 99ms,則最後一次丟失了。
  2. 例如要用滾動條離頂部的高度來設置樣式,滾動條在 99ms 從 0 滾動到 100px 處,你沒辦法處理。
  • PS: 時間戳版,有一個應用場景,在一定時間內防止重複提交。

  • 定時器版

  1. 首次 0ms 不會立即執行有 100ms 延遲,好比開第一槍需要 100ms 後子彈才能出來。
  • 聰明的讀者,可能想到了,可以結合兩者來解決問題。
  1. 首次 0ms 立即執行無延遲;
  2. 獲取最後狀態,保證最後一次得到執行。

案例

  • 滾動滑動條時視覺上連續調整 dom
/**
* 時間戳來處理首次和間隔執行問題
* 定會器來確保最後一次狀態改變得到執行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
  let timer, lastTime;
  return function() {
    const now = new Date().getTime();
    const space = now - lastTime; // 間隔時間
    if( lastTime && space < delay ) { // 爲了響應用戶最後一次操作
      // 非首次,還未到時間,清除掉計時器,重新計時。
      timer && clearTimeout(timer);
      // 重新設置定時器
      timer = setTimeout(() => {
        lastTime = now; // 不要忘了記錄時間
        fn.apply(this, arguments);
      }, delay);
      return;
    }
    // 首次或到時間了
    lastTime = now;
    fn.apply(this, arguments);
  };
}

const throttleAjax = throttle(ajax, 100);

window.addEventListener('scroll', function() {
  const top = document.body.scrollTop || document.documentElement.scrollTop;
  throttleAjax('scrollTop: ' + top);
});
  • 節流前

回調過於密集。(PS:經常聽到 scroll 自帶節流,就是指一幀 16ms 左右觸發一次)

在這裏插入圖片描述

  • 節流後

可以發現,無論你滑的慢還是快都類似於定時觸發。

在這裏插入圖片描述

  • 細心的讀者可能會發現,假如交互停留在 199ms,定時器在 300ms 段才執行,間隔了約 200ms,定時器延遲不應該設置爲原來的 delay

在這裏插入圖片描述

/**
* 時間戳來處理首次和間隔執行問題
* 定會器來確保最後一次狀態改變得到執行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
  let timer, lastTime;
  return function() {
    const now = new Date().getTime();
    const space = now - lastTime; // 間隔時間
    if( lastTime && space < delay ) { // 爲了響應用戶最後一次操作
      // 非首次,還未到時間,清除掉計時器,重新計時。
      timer && clearTimeout(timer);
      // 重新設置定時器
      timer = setTimeout(() => {
        lastTime = now; // 不要忘了記錄時間
        fn.apply(this, arguments);
-      }, delay);
+      }, delay - space);
      return;
    }
    // 首次或到時間了
    lastTime = now;
    fn.apply(this, arguments);
+    // 當前已執行,清除掉計時器,不清除會有多餘的中間執行
+    timer && clearTimeout(timer);
  };
}
  • 如果忘了最後清除

在這裏插入圖片描述

  • 最終效果

在這裏插入圖片描述

  • 可以發現時間沒有改之前那麼準確,說明有些細節還是沒有拿捏好,這裏就不繼續討論下去了。

  • 實際生產還是使用 lodash 成熟可靠的的防抖節流實現。

  • lodash 效果

在這裏插入圖片描述

  • 查看 lodash 源碼可以發現節流,是靠 leading 來控制首次是否需要執行,trailing 來控制 99ms 停止 100ms時需不需要執行,
    maxWait 來控制定時執行,看完本篇去分析,是不是就很好理解了呢。

  • 舉幾個常用的 lodash 使用方式。

  1. 實現類似 scroll 自帶一幀 16ms 效果:throttle(fn, /* wait = undefined */),其實內部用到了 requestAnimationFrame
  2. 非常常用,發請求防止重複提交,例如首次點擊執行,500ms 內的點擊一律不執行: debounce(fn, 500, { leading: true, trailing: false })
  3. 例如某個 dom 被清除,debounced.cancel() 來取消最後一次(trailing)調用,避免取不到 dom 報錯。
  4. 等等。

總結

防抖、節流都是利用閉包來實現內部數據獲取與維護。
防抖比較好理解,節流就需要稍微需要思考下。兩者還是有區別的,就不要一錯再錯,粘貼傳播問題代碼啦。
防抖、節流對於頻繁dom事件性能優化是不可或缺的手段。

參考

  1. 文中 demo
  2. 7分鐘理解JS的節流、防抖及使用場景
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章