老生常談的函數防抖與節流

  • 2019 年 10 月 7 日
  • 筆記

https://github.com/ZengLingYong/Blog/issues/21

本篇課題,或許早已是爛大街的解讀文章。不過一系列面試下來,不少夥伴們還是似懂非懂地栽倒在(~面試官~)深意的笑容之下,權當溫故知新。

JavaScript的執行過程,是基於棧來進行的。複雜的程式程式碼被封裝到函數中,程式執行時,函數不斷被推入執行棧中。所以 "執行棧" 也稱 "函數執行棧"。

函數中封裝的程式碼塊,一般都有相對複雜的邏輯處理(計算/判斷),例如函數中可能會涉及到 DOM 的渲染更新,複雜的計算與驗證, Ajax 數據請求等等。

前端頁面的操作權,大部分都是屬於瀏覽端的客戶爸爸們(單身三十年的手速,惹不起惹不起!!!)。如果函數被頻繁調用,造成的性能開銷絕對不只一點點。

  • 前: DOM 頻繁重繪的卡頓讓客戶爸爸們想把你揪出來一頓大招。。。
  • 後:後端同學正在提刀趕來的路上:「為什麼我的介面被你玩掛了」。。。

既要提升用戶體驗,又要減少後端服務開銷,可見我們大前端的使命不只一頁PPT。說好前因,接著就是後果了。既然有優化的需求,必然就要有相應的解決方案。隆重請出主角: 「防抖」 與 「節流」。

防抖(debounce)

在事件被觸發 n 秒後再執行回調函數,如果在這 n 秒內又被觸發,則重新計時延遲時間。

生活化理解:英雄的技能條,技能條讀完才能使用技能(R大招60s)

防抖的實現方式分兩種 「立即執行」 和 「非立即執行」,區別在於第一次觸發時,是否立即執行回調函數。

非立即執行

」非立即執行防抖「 指事件觸發後,回調函數不會立即執行,會在延遲時間 n 秒後執行,如果 n 秒內被調用多次,則重新計時延遲時間

// e.g. 防抖 - 非立即執行  function debounce(func, delay) {    var timeout;    return function() {      var context = this;      var args = arguments;      // && 短路運算 == if(timeout) else {...}      timeout && clearTimeout(timeout);      timeout = setTimeout(function(){        func.apply(context, args);      }, delay);    }  }    // 調用  var printUserName = debounce(function(){    console.log(this.value);  }, 800);  document.getElementById('username')    .addEventListener('keyup', printUserName);

立即執行

「立即執行防抖」 指事件觸發後,回調函數會立即執行,之後要想觸發執行回調函數,需等待 n 秒延遲

// e.g. 防抖 - 立即執行  function debounce(func, delay) {      var timeout;      return function() {          var context = this;          var args = arguments;          callNow = !timeout;          timeout = setTimeout(function() {              timeout = null;          }, delay);          callNow && func.apply(context, args);      }  }

函數防抖原理:通過維護一個定時器,其延遲計時以最後一次觸發為計時起點,到達延遲時間後才會觸發函數執行。

節流(throttle)

規定在一個單位時間內,只能觸發一次函數。如果這個單位時間內觸發多次函數,只有一次生效(間隔執行)

生活化理解:

  1. FPS射擊遊戲子彈射速(即使按住滑鼠左鍵,射出子彈的速度也是限定的)
  2. 水龍頭的滴水(水滴攢到一定重量才會下落)

函數節流實現的方式有 「時間戳」 和 「定時器」 兩種。

時間戳

// e.g. 節流 - 時間戳  function throttle(func, delay) {    var lastTime = 0;    return function() {      var context = this;      var args = arguments;      var nowTime = +new Date();      if (nowTime > lastTime + delay) {        func.apply(context, args)        lastTime = nowTime;      }    }  }

「時間戳」 的方式,函數在時間段開始時執行。

缺點:假定函數間隔1s執行,如果最後一次停止觸發,卡在4.2s,則不會再執行。

定時器

// e.g. 節流 - 定時器  function throttle(func, delay) {    var timeout;    return function() {      var context = this;      var args = arguments;      if (!timeout) {        setTimeout(function(){          func.apply(context, args);          timeout = null;        }, delay)      }    }  }

「定時器」 的方式,函數在時間段結束時執行。可理解為函數並不會立即執行,而是等待延遲計時完成才執行。(由於定時器延時,最後一次觸發後,可能會再執行一次回調函數)

時間戳 + 定時器(互補優化)

// e.g. 節流 - 時間戳 + 定時器  function throttle(func, delay) {    let lastTime, timeout;    return function() {      let context = this;      let args = arguments;      let nowTime = +new Date();      if (lastTime && nowTime < lastTime + delay) {        timeout && clearTimeout(timeout);        timeout = setTimeout(function(){          lastTime = nowTime;          func.apply(context, args);        }, delay);      } else {        lastTime = nowTime;        func.apply(context, args);      }    }  }

合併優化的原理:「時間戳」方式讓函數在時間段開始時執行(第一次觸發立即執行),「定時器」方式讓函數在最後一次事件觸發後(如4.2s)也能觸發。

函數節流原理:一定時間內只觸發一次,間隔執行。通過判斷是否到達指定觸發時間,間隔時間固定。

「防抖」 與 「節流」 的異同

相同:都是防止某一時間段內,函數被頻繁調用執行,通過時間頻率控制,減少回調函數執行次數,來實現相關性能優化。

區別:「防抖」是某一時間內只執行一次,最後一次觸發後過段時間執行,而「節流」則是間隔時間執行,間隔時間固定。

「防抖」 與 「節流」 的應用場景

防抖

  1. 文本輸入搜索聯想
  2. 文本輸入驗證(包括 Ajax 後端驗證)

節流

  1. 滑鼠點擊
  2. 監聽滾動 scroll
  3. 窗口 resize
  4. mousemove 拖拽

應用場景還有很多,具體場景需具體分析。只要涉及高頻的函數調用,都可參考函數防抖節流的優化方案。

鼓起勇氣寫在結尾:以上程式碼都不是 「完美」 的 「防抖 / 節流」 實現程式碼!!!僅就實現方式和基本原理,淺談分解一二。

實際程式碼開發中,一般會引入lodash 相對 「靠譜」 的第三方庫,幫我們去實現防抖節流的工具函數。有興趣的夥伴們可閱讀 lodash 相關源碼,加深印象理解可再讀以下參考文章。