以和產品撕逼爲例學習 debounce 和 throttle

前言

這兩個函數網上已經有不少實現了, 通常項目中直接用 lodash 或 underscore 的實現git

所以,寫出一個完善的 debounce 和 throttle 不是本篇的目的github

理解這兩個方法的實現思路,清楚使用場景纔是重點chrome

debounce

概念

防抖:你儘管觸發(通知我要執行該函數),我執行算我輸(誤,,等你累了(離最後一次觸發過了 wait 時間),我再執行微信

這裏的 執行 表示函數的實際調用, 而 觸發 僅僅是通知執行app

以坐電梯爲例,電梯運行表示函數執行,有人進電梯表示一次觸發:通知電梯運行。一段時間內沒人進電梯,那麼電梯就開始運行。函數

PS: 沒人進電梯那麼電梯也不會運行佈局

實現

仍是以坐電梯爲例,咱們建立如下實體類測試

class Elevator {
  /** * @param {number} no 電梯編號 */
  constructor(no) {
    this.no = no
  }
  run () {
    console.log(`${this.no}號電梯開始運行`)
  }
}
class People {
  constructor(no) {
    this.name = "員工" + no
  }
  into (elevator) {
    console.log(`${this.name} 進入${elevator.no}號電梯`)
    elevator.run()
  }
}
複製代碼

運行優化

let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)
// 員工0 進入0號電梯
// 0號電梯開始運行
new People(index++).into(elevator)
// 員工1 進入0號電梯
// 0號電梯開始運行
複製代碼

有人進就立刻運行電梯,但現實 run 執行函數(電梯運行)成本是巨大的,員工1也不可能進入電梯。ui

所以咱們不能輕易的運行電梯。而且上面的實現,用戶應該是不能直接讓電梯運行的,只能通知電梯有人進電梯了。

咱們進行以下改造:

編寫防抖函數

function debounce (func, wait) {
  let timer = null
  return function () {
    clearTimeout(timer)
    timer = setTimeout(()=>{
      console.log('防抖完畢..開始執行')
      func()
    }, wait);
  }
}
複製代碼

改造 Elevator 和 People

class Elevator {
  /** * @param {number} no 電梯編號 */
  constructor(no) {
    this.no = no
    // 對外提供的接口,用戶告知電梯該運行了
    this.notify = debounce(this._run, 3000)
  }
  // 僞裝是私有方法,只能我本身調用
  _run () {
    console.log(`${this.no}號電梯開始運行`)
  }
}
class People {
  constructor(no) {
    this.name = "員工" + no
  }
  into (elevator) {
    console.log(`${this.name} 進入${elevator.no}號電梯`)
    elevator.notify()
  }
}
複製代碼

剛剛的例子再從新運行一次.

let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)
new People(index++).into(elevator)
複製代碼

不出意外,報了 Uncaught TypeError: Cannot read property 'no' of undefined

很明顯,_run 方法執行的時候裏面的 this 值爲 undefined

緣由在於 setTimeout 中 func 的調用方爲全局做用域,在嚴格模式 (class 中的代碼處於嚴格模式)下函數的 this 爲 undefined

解法有多種:

  1. this.notify = debounce(this._run.bind(this), 3000)

這樣至關於對外部使用者進行了要求:必須進行 bind ,其實不太好

  1. func.call(this)

此處的 this 指向爲 notify 的調用方
注意 setTimeout 用的是箭頭函數,不然 setTimeout 內函數的 this 是 window

與此同時,若是對 elevator.notify 進行傳參的話,func 調用時忽略掉了!

所以對 debounce 進行以下改造:

function debounce (func, wait) {
  let timer = null
  return function () {
    clearTimeout(timer)
    timer = setTimeout(()=>{
      console.log('防抖完畢..開始執行')
      func.apply(this,arguments)
    }, wait);
  }
}
複製代碼

其餘代碼調整了下輸出:

class Elevator {
  /** * @param {number} no 電梯編號 */
  constructor(no) {
    this.no = no
    // 對外提供的接口,用戶告知電梯該運行了
    this.notify = debounce(this._run, 3000)
  }
  // 僞裝是私有方法,只能我本身調用
  _run (...args) {
    console.log("最後一次調用傳入的參數爲:",args)
    console.log(`${this.no}號電梯開始運行`)
  }

}
class People {
  constructor(no) {
    this.name = "員工" + no
  }
  into (elevator) {
    console.log(`${this.name} 進入${elevator.no}號電梯`)
    elevator.notify(this.name)
  }
}
複製代碼

測試輸出

let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)

new People(index++).into(elevator)

setTimeout(() => {
  new People(index++).into(elevator)
}, 1000);
new People(index++).into(elevator)

// 員工0 進入0號電梯
// 員工1 進入0號電梯
// 員工2 進入0號電梯

// ... 等待1s

// 員工3 進入0號電梯

// ... 等待3s

// 防抖完畢..開始執行
// 最後一次調用傳入的參數爲: ["員工3"]
// 0號電梯開始運行
複製代碼

至此,咱們的實現就能達到基本需求了。 若是看了 underscore 等開源庫的話,會發現它還實現了其餘需求

  1. 馬上執行(leading=true):當即執行 func 方法,隨後進行的每一次調用,只有超過 wait 時間沒有再次調用,纔會執行。

經常使用場景:初次點擊搜索框控件,進行一次查詢

  1. 禁用結束後的回調(trailing=false):超過 wait 時間沒有再次調用,不進行執行,但容許下次當即執行(配置 leading=true 的話)。
  2. 返回值:當採用當即執行模式時,須要獲取函數執行的返回值

初次查詢獲取到完整列表

  1. 取消防抖: 爲了馬上執行模式時快速執行,避免還須要等待 wait 時間;非馬上執行模式下至關於清空原來的狀態

仍是以上電梯爲例,這裏爲了方便理解,咱們所說的電梯執行,是快速把人送到又回來等待別人進入。

  • leading=false;trailing=true【lodash 默認設置】: 上文的例子,第一我的進不會立刻運行,超過 wait 時間都沒人進電梯,那電梯就開始運行
  • leading=true;trailing=true: 第一我的進電梯後,電梯立刻運行。若是第二我的在 wait 時間內進入電梯,那麼開始防抖處理,超過 wait 時間都沒人進電梯,那電梯纔開始運行;若是第二我的距離第一我的進電梯的時間大於 wait (好比比較晚來,好比電梯執行比較慢等因素) 那麼他和第一人同樣,直接進入電梯並讓電梯執行。
  • leading=true;trailing=false: 第一我的進電梯後,電梯立刻運行。若是第二我的在 wait 時間內進入電梯,那麼開始防抖處理,超過 wait 時間都沒人進電梯,電梯會作個判斷,下次再有一我的進,電梯立刻開走,以後進電梯的人就和當前的第二我的一樣處理;若是若是第二我的距離第一我的進電梯的時間大於 wait ,那麼他和第一人同樣,直接進入電梯並讓電梯執行。
  • leading=false;trailing=false: 電梯永遠不運行。。。

取消防抖:電梯運行前要等 wait 時間,這時候電梯有個功能,按了某個按鈕後,不用等 wait 時間,只要有新的人進電梯電梯立馬運行(leading=true),或者從新開始防抖處理(leading=false)

根據需求進行配置,能夠看出來,咱們比較經常使用的是第一種,這也是 lodash 的默認設置

固然,上面這些需求的實現不是本文的重點,感興趣的話能夠直接看開源庫源碼和文章底部的拓展閱讀,其實不會很難~

  1. lodash-debounce 使用文檔
  2. underscore-debounce github
  3. lodash-debounce github

throttle

概念

節流:顧名思義,用來減小函數的執行次數的,固定過一段時間後纔會執行。

以和產品撕逼爲例,作需求表示函數執行,提需求表示一次觸發:通知你作需求。產品初次給你提了一個需求,但是你很忙(你以爲有坑),你讓TA理清了再來,過段時間你再作(你是有原則的,從第一次提需求開始固定時間後你必定去作),這段時間產品能夠對需求進行變動優化 ~ 。 而後產品又給你提了一個需求……

PS: 若是產品沒提需求,那天然也不用作了

試想一下,這裏若是用防抖的場景會如何?

是否是就像產品時不時的給你改需求,你每次都得從新設計方案- -。直到好久沒改需求了,你纔開始處理需求。

實現

仍是以作需求爲例,咱們建立如下實體類

/** * 研發 */
class RD {
  /** * @param {number} no 研發編號 */
  constructor(no) {
    this.name = `研發` + no
    // 用於需求方通知開發處理需求
    this.notify = this._processing
  }
  // 僞裝是私有方法,只能研發本身調用
  _processing (...args) {
    console.log("需求文檔:", args)
    console.log(`${this.name}開始處理需求`)
  }

}
/** * 產品經理 */
class PM {
  constructor(no) {
    this.name = "產品經理" + no
  }
  request (rd, requirement) {
    console.log(`${this.name} 請求 ${rd.name} 實現 ${requirement}`)
    rd.notify(requirement)
  }
}
複製代碼

在不進行節流的狀況下,場景以下

let rd = new RD(0)
let pm = new PM(0)

pm.request(rd,"微信APP")
pm.request(rd,"抖音APP")

// 產品經理0 請求 研發0 實現 微信APP
// 需求文檔: ["微信APP"]
// 研發0開始處理需求
// 產品經理0 請求 研發0 實現 抖音APP
// 需求文檔: ["抖音APP"]
// 研發0開始處理需求
複製代碼

研發估計得累死...

進行防抖的話呢?

// RD 中進行以下修改
this.notify = debounce(this._processing,5000)

function debounce (func, wait) {
  let timer = null
  return function () {
    console.log('研發收到需求:',arguments)
    clearTimeout(timer)
    timer = setTimeout(()=>{
      func.apply(this,arguments)
    }, wait);
  }
}
複製代碼

效果以下:

let rd = new RD(0)
let pm = new PM(0)

pm.request(rd,"微信APP")
pm.request(rd,"抖音APP")

// 產品經理0 請求 研發0 實現 微信APP
// 研發收到需求: ["微信APP"]
// 產品經理0 請求 研發0 實現 抖音APP
// 研發收到需求: ["抖音APP"]

// 過了5s...

// 需求文檔: ["抖音APP"]
// 研發0開始處理需求
複製代碼

雖然還沒開始處理,但不斷被告知修改需求,也累的夠嗆

那換成節流呢?

// RD 中進行以下修改
// 表示初次接收到需求後,5s後開發必定會去作
this.notify = throttle(this._processing,5000)

function throttle (func, wait) {
  let timer = null
  return function () {
    if (!timer) {
      console.log('研發收到需求:', arguments)
      timer = setTimeout(() => {
        func.apply(this, arguments)
        timer = null
      }, wait);
    }

  }
}
複製代碼

操做以下

let rd = new RD(0)
let pm = new PM(0)

pm.request(rd, "微信APP")
pm.request(rd, "抖音APP")

/*** 第0s ***/

// 產品經理0 請求 研發0 實現 微信APP
// 研發收到需求:["微信APP"]
// 產品經理0 請求 研發0 實現 抖音APP

/*** 第5s ***/

// 需求文檔: ["微信APP"]
// 研發0開始處理需求
複製代碼

好像有哪裏不對?研發作的怎麼是 微信APP 的需求,說明 func.apply(this, arguments) 傳遞的參數 arguments 不對

緣由在於箭頭函數沒有本身的 this 和 arguments ,因此該函數內這兩個的值是拿的上層做用域 function 函數中的值,最關鍵的是,這個值是聲明時肯定而不是執行時肯定的。

因爲該箭頭函數只在第一次 timer 爲空的時候被聲明,所以箭頭函數裏面的 arguments 的值就沒有再改過了

咱們作個改造,將 arguments 提到上層做用域中

function throttle (func, wait) {
  let timer = null
  let args = []
  return function () {
    args = arguments
    if (!timer) {
      console.log('研發收到需求:', args)
      timer = setTimeout(() => {
        func.apply(this, args)
        timer = null
      }, wait);
    }
  }
}
複製代碼
let rd = new RD(0)
let pm = new PM(0)

pm.request(rd, "微信APP")
pm.request(rd, "抖音APP")
setTimeout(()=>{
  pm.request(rd, "今日頭條APP")
},2000)
setTimeout(()=>{
  pm.request(rd, "chrome app")
},6000)

/*** 第0s ***/

// 產品經理0 請求 研發0 實現 微信APP
// 研發收到需求:["微信APP"]
// 產品經理0 請求 研發0 實現 抖音APP

/*** 第2s ***/

// 產品經理0 請求 研發0 實現 今日頭條APP

/*** 第5s ***/

// 需求文檔: ["今日頭條APP"]
// 研發0開始處理需求

/*** 第6s ***/

// 產品經理0 請求 研發0 實現 chrome app
// 研發收到需求: ["chrome app"]

/*** 第11s ***/

// 需求文檔: ["chrome app"]
// 研發0開始處理需求
複製代碼

至此,節流的基本功能就開發完成了。對比下開源實現,咱們還缺的功能有:

  1. 當即執行 options.leading=true: 咱們上面的實現就是 options.leading=false 的效果
  2. 禁用結束後的回調 options.trailing=false: 咱們上面的實現就是 options.trailing=true 的效果,即時間一到就會進行函數的執行。禁用後時間一到不會再執行一次
  3. 返回結果:當即執行模式時能夠獲取到結果
  4. 取消節流:當採用當即執行模式時,要過一段時間才能從新觸發函數執行,取消節流後就函數觸發就會立刻執行

以作需求爲例,

  • leading=false,trailing=true: 上文的例子,研發固定過段時間纔開始作需求
  • leading=true,trailing=false: 你只作初版的需求,在一段時間內產品改需求你都不理會TA
  • leading=true,trailing=true【lodash 默認設置】: 你先作了初版的需求,在一段時間內產品不斷改需求,你最後會再作一次需求
  • leading=false,trailing=false: 你啥也不作~嘻嘻

因此,通常狀況下咱們不能同時設置 leadingtrailing 爲 false。

返回結果 的意思就是:產品要求你作的初版需求立刻出效果

取消節流 的意思就是:產品告訴你領導你在偷懶,下次你立刻就收到產品的需求了,若是 leading=true ,那下一次需求立刻解決,不然仍是等待 wait 再作

相關的開源庫源碼能夠參考:

  1. underscore-throttle github
  2. lodash-throttle github

區別

接下來講下二者的區別吧,其實能夠用一句話歸納,最終什麼時候執行取決於發起方仍是執行方

取決於發起方那麼是 debounce ,取決於執行方那麼是 throttle

仍是用作需求爲例,debounce 的狀況,研發偏向產品一段時間後不改需求才開始作需求,若是產品不斷的改需求,那研發作需求的時間是不能控制的

而 throttle 的狀況,研發偏向固定時間段後才作需求,這個時間段中,產品該不應需求都不影響我何時作需求

經常使用的使用場景

如下幾個場景,使用哪一種策略更好,以及對應的配置項

  1. 搜索框的篩選
  2. 搶票按鈕
  3. 發送短信驗證碼
  4. 元素拖拽
  5. 窗口 resize,調整佈局

這裏就不給出答案了,歡迎評論~

這裏是 github 上的最新原文

拓展閱讀

  1. JavaScript專題之跟着 underscore 學防抖
  2. JavaScript專題之跟着 underscore 學節流
相關文章
相關標籤/搜索