天天閱讀一個 npm 模塊(4)- throttle-debounce

系列文章:javascript

  1. 天天閱讀一個 npm 模塊(1)- username
  2. 天天閱讀一個 npm 模塊(2)- mem
  3. 天天閱讀一個 npm 模塊(3)- mimic-fn

上一篇文章中介紹的屬性描述符的知識太偏於理論,今天閱讀的 throttle-debounce 模塊會實用許多,在工做經常能夠用到。java

一句話介紹

今天閱讀的 npm 模塊是 throttle-debounce,它提供了 throttledebounce 兩個函數:throttle 的含義是節流,debounce 的含義是防抖動,經過它們能夠限制函數的執行頻率,避免短期內函數屢次執行形成性能問題,當前包版本爲 2.0.1,周下載量爲 6.3 萬。git

用法

首選須要介紹一下 throttledebounce ,它們均可以用於 函數節流 從而提高性能,但它們仍是存在一些不一樣:github

  • debounce:將短期內屢次觸發的事件合併成一次事件響應函數執行(每每是在第一次事件或者在最後一次事件觸發時執行),即該段時間內僅一次真正執行事件響應函數。
  • throttle:假如在短期內同一事件屢次觸發,那麼每隔一段更小的時間間隔就會執行事件響應函數,即該段時間內可能屢次執行事件響應函數。

雖然天天最煩等電梯要花上十幾分鍾,但仍是能夠用坐電梯來舉例子:npm

  • debounce:假如我在電梯裏面正準備關門,這時 A 想要坐電梯,那麼出於禮貌我會按下開門鍵,而後等他走進電梯後再嘗試關門;等 A 進電梯後,又發現 B 也想要坐電梯,那麼一樣出於禮貌我會按下開門鍵,而後等他走進電梯。那麼假如一直有人想要坐電梯的話,我就會不斷地延後按下關門鍵的時機,直至沒有人想要坐電梯(現實生活中我這樣作的話,估計天天除了坐電梯就能夠什麼都不作了)。
  • throttle:實際上我天天都有工做要完成,不可能在電梯裏無限地等別人。那麼這回我任性一點,規定我只等 30 秒,無論到時候有沒有人想要坐電梯,我都會按下關門鍵走掉。

從上面兩個例子中能夠看出二者最大的區別在於只要有事件發生(有人想坐電梯),若使用了 throttle 方法,那麼在一段時間內事件響應函數必定會執行(30秒內我按下關門鍵);若使用了 debounce 方法,那麼只有事件中止發生後(我發現沒有人想坐電梯)纔會執行。segmentfault

你們能夠嘗試在下面的 Demo 中滾動鼠標直觀地感覺到這二者的不一樣:瀏覽器

See the Pen The Difference Between Throttling, Debouncing, and Neither by Elvin Peng (@elvinn) on CodePen.服務器

對於 throttle-debounce,它的簡單用法以下:閉包

import { throttle, debounce } from 'throttle-debounce';

function foo() { console.log('foo..'); }
function bar() { console.log('bar..'); }

const fooWrapper = throttle(200, foo);

for (let i = 1; i < 10; i++) {
  setTimeout(fooWrapper, i * 30);
}

// => foo 執行了三次
// => foo..
// => foo..
// => foo..

const barWrapper = debounce(200, bar);

for (let i = 1; i < 10; i++) {
  setTimeout(barWrapper, i * 30);
}

// => bar 執行了一次 
// => bar..

複製代碼

源碼學習

throttle 實現

將源碼簡化後適當修改以下:app

// 源碼 4-1
function throttle(delay, callback) {
  let timeoutID;
  let lastExec = 0;

  function wrapper() {
    const self = this;
    const elapsed = Number(new Date()) - lastExec;
    const args = arguments;

    function exec() {
      lastExec = Number(new Date());
      callback.apply(self, args);
    }

    clearTimeout(timeoutID);

    if (elapsed > delay) {
      exec();
    } else {
      timeoutID = setTimeout(exec, delay - elapsed);
    }
  }

  return wrapper;
}
複製代碼

整個代碼的邏輯十分清晰,一共只有三步:

  1. 計算距離最近一次函數執行後通過的時間 elapsed,並清除以前設置的計時器。
  2. 若是通過的時間大於設置的時間間隔 delay,那麼當即執行函數,並更新最近一次函數的執行時間。
  3. 若是通過的時間小於設置的時間間隔 delay,那麼經過 setTimeout 設置一個計數器,讓函數在 delay - elapsed 時間後執行。

源碼 4-1 並不難理解,不過須要關注一下 this 的使用:

function throttle(delay, callback) {
    // ...
    function wrapper() {
    	const self = this;
        const args = arguments;
        // ...
        
        function exec() {
            // ...
	      	callback.apply(self, args);
    	}
        
    }
}
複製代碼

在上面的代碼中,經過 self 變量臨時保存 this 的值,從而在 exec 函數中經過 callback.apply(self, args) 傳入正確的 this 值,這種作法在閉包相關的函數調用中十分經常使用。正由於這裏對 this 的處理,因此能夠實現下面的能力:

function foo() { console.log(this.name);  }

const fooWithName = throttle(200, foo);

const obj = {name: 'elvin'};

fooWithName.call(obj, 'elvin');

// => 'elvin'
複製代碼

debounce 實現

因爲 debouncen 只是日後推延函數的執行時間,並不具備 throttle 每隔一段時間必定會執行的能力,因此其實現起來更加簡單:

function debounce(delay, callback) {
  let timeoutID;

  function wrapper() {
    const self = this;
    const args = arguments;

    function exec() {
      callback.apply(self, args);
    }

    clearTimeout(timeoutID);

    timeoutID = setTimeout(exec, delay);
  }

  return wrapper;
}
複製代碼

將上述代碼與 throttle 實現的代碼相比,能夠發現其就是去除了 elapsed 相關邏輯後的代碼,其他大部分代碼如出一轍,因此 debounce 函數能夠藉助 throttle 函數實現(throttle-debounce 源代碼中也是這樣作的),throttle 函數也能夠藉助 debounce 函數實現。

throttle 和 debounce 使用場景舉例

throttledebounce 適用於用戶短期內頻繁執行某一相同操做的場景,例如:

  • 用戶拖動瀏覽器窗口改變窗口大小,觸發 resize 事件。
  • 用戶移動鼠標,觸發 mousemove 等事件。
  • 用戶在輸入框內進入輸入,觸發 keydown | keypress | keyinput | keyup 等事件。
  • 用戶滾動屏幕,觸發 scroll 事件。
  • 用戶在點擊按鈕後,因爲 API 請求耗時未當即看到響應,可能會不斷點擊按鈕觸發 click 事件。

在網上搜索了很多資料,發現對兩個函數的使用場景有時彼此之間都互相矛盾,例若有的說在搜索框進行輸入,應該使用 debounce 進行限流,從而減輕服務器壓力;有的說使用 throttle 進行限流便可,能夠更快地返回用戶的搜索結果。

在我看來,並不存在一個場景,就必定是使用 throttledebounce 中的一種方法並另一種方法好,每每須要結合自身的狀況進行考慮和選擇:

  • 當事件響應函數對 CPU、GPU、流量、服務器等資源的佔用在接受範圍內時,可使用 throttle 進行限流帶來更好的用戶體驗。
  • 當事件響應函數對 CPU、GPU、流量、服務器等資源的佔用較大時,可使用 debounce 進行更強力的限流,從而減輕壓力。

寫在最後

throttle-debounce 源碼和我前幾天所看的 Sindre 所寫的模塊代碼風格徹底不一樣,它的代碼中註釋的行數約爲代碼行數的三倍,並且函數的參數均有詳細的註釋,這本應是一件好事,可是對於我閱讀源碼而言,並無以爲更加輕鬆,而求因爲對可選參數進行的以下處理,讓我閱讀起來更加費力:

// 源碼 4-2

/** * * @param {Number} delay * @param {Boolean} [noTrailing] * @param {Function} callback * @param {Boolean} [debounceMode] * * @return {Function} A new, throttled, function. */
export default function ( delay, noTrailing, callback, debounceMode ) {
    // `noTrailing` defaults to falsy.
	if ( typeof noTrailing !== 'boolean' ) {
		debounceMode = callback;
		callback = noTrailing;
		noTrailing = undefined;
	}
    
    // ...
}
複製代碼

在源碼 4-2 中,從註釋能夠看出 noTrailingdebounceMode 是可選參數,delay 和 callback 爲必選參數,而後它將可選參數 noTrailing 放在了必選參數 callback 以前,再在函數中的代碼進行判斷:假如 noTrailing 爲函數的話,則此值應做爲 callback,而後再將 noTrailing 設爲默認值 undefined

不由感嘆這真是一番騷操做,哪怕是爲了兼容 ES5,也有更好的寫法,這裏說說我我的認爲可用 ES6 語法時更好的寫法:

export default function (dalay, noTrailing, options = { callback = false, debounceMode = false, } = {}) {
    // ...
}
複製代碼

關於我:畢業於華科,工做在騰訊,elvin 的博客 歡迎來訪 ^_^

相關文章
相關標籤/搜索