說說JavaScript中函數的防抖 (Debounce) 與節流 (Throttle)

爲何要防抖和節流

有時候會在項目開發中頻繁地觸發一些事件,如 resizescrollkeyupkeydown等,或者諸如輸入框的實時搜索功能,我們知道如果事件處理函數無限制調用,會大大加重瀏覽器的工作量,有可能導致頁面卡頓影響體驗;後臺接口的頻繁調用,不僅會影響客戶端體驗,還會大大增加服務器的負擔。而如果對這些調用函數增加一個限制,讓其減少調用頻率,豈不美哉?

針對這個問題,一般有兩個方案:
防抖 (Debounce)
節流 (Throttle)

防抖(Debounce)

我對函數防抖的定義:當函數被連續調用時,該函數並不執行,只有當其全部停止調用超過一定時間後才執行1次。

一個被經常提起的例子:

上電梯的時候,大家陸陸續續進來,電梯的門不會關上,只有當一段時間都沒有人上來,電梯纔會關門。

Talk is cheap,我們直接 show code 吧。

先做基本的準備(篇幅原因,HTML部分省略):

let container = document.getElementById('container');

// 事件處理函數
function handle(e) {
    onsole.log(Math.random()); 
}

// 添加滾動事件
container.addEventListener('scroll', handle);

我們發現,每滾動一下,控制檯就會打印出一行隨機數。

基礎防抖

我們現在寫一個最基礎的防抖處理:

function debounce(func, wait) {
    var timeout;//標記
    return function() {
      clearTimeout(timeout);
      timeout = setTimeout(func, wait);
    }
}

事件也做如下改寫:

container.addEventListener('scroll', debounce(handle, 1000));

現在試一下, 我們會發現只有我們停止滾動1秒鐘的時候,控制檯纔會打印出一行隨機數。

標準防抖

以上基礎版本會有兩個問題,請看如下代碼:

// 處理函數
function handle(e) {
    console.log(this); //輸出Window對象
    console.log(e); //undefined
}

沒錯,當我們不使用防抖處理時,handle()函數的this指向調用此函數的container,而在外層使用防抖處理後,this的指向會變成Window。
其次,我們也要獲取到事件對象event

所以我們要對防抖函數做以下改寫:


function debounce(fn, wait) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(()=>{
      fn.apply(this,arguments)//使用apply改變this指向
    }, wait);
  }
}

當然了,如果使用箭頭函數便可以省去外層聲明。

先觸發式防抖

以上的情況都是隻有當連續觸發停止後才執行,那如果我們想讓事件第一次觸發就執行,後面的連續觸發都不執行,直到停止觸發一段時間纔可以再次觸發(比如防止頻繁點擊),該如何處理呢?

那麼可以利用同樣的原理,稍作修改即可:

function debounce(fn, wait) {
    let timeout;
    return function(){
      let arg = arguments;
      let that = this;
      clearTimeout(timeout);
      !timeout && fn.apply(that,arg)
        timeout = setTimeout(function(){
          timeout = null;
        }, wait);
    }
}

節流 (Throttle)

顧名思義,節流就是節約流量,將連續觸發的事件稀釋成預設評率。
比如每間隔1秒執行一次函數,無論這期間觸發多少次事件。

這有點像公交車, 無論在站點等車的人多不多,公交車只會按時來一班,不會來一個人就來一輛公交車。

標準節流

function throttle(fn, wait) {
  let timeout; 
  return function () {
    if (!timeout) { 
      timeout = setTimeout(() => {
        timeout = null;
        fn.apply(this, arguments)
      }, wait)
    }
  }
}

用滾動事件來描述節流,其實是一個非常典型的場景,比如需要用滾動事件判斷是否加載更多等。

先觸發式節流

和防抖函數類似,以上的情況是先等待後觸發,如果我們想讓事件先觸發後等待,該如何處理呢?網上大部分文章都告訴你用時間戳的方式去實現,其實只要像防抖一樣稍作修改即可實現。

function throttle(fn, wait) {
  let timeout; 
  return function () {
    if (!timeout) { 
      fn.apply(this, arguments)
      timeout = setTimeout(() => {
        timeout = null;
      }, wait)
    }
  }
}

這樣,我們就會發現第一次觸發函數就會立即生效。

總結

關於防抖與節流,lodash、underscore等工具庫都有完善的實現可以直接用,本沒有必要造輪子。本文的目的僅僅是爲了將其主要思想和實現思路展現出來。更重要的,知道防抖和節流的本質後,就知道在何時使用防抖或者節流,何時先觸發或後觸發。無論需求如何改變,都可以靈活的運用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章