原文:http://blog.csdn.net/redtopic/article/details/69396722
在處理諸如 resize、scroll、mousemove 和 keydown/keyup/keypress 等事件的時候,通常我們不希望這些事件太過頻繁地觸發,尤其是監聽程序中涉及到大量的計算或者有非常耗費資源的操作。
有多頻繁呢?以 mousemove 為例,根據 DOM Level 3 的規定,「如果鼠標連續移動,那么瀏覽器就應該觸發多個連續的 mousemove 事件」,這意味着瀏覽器會在其內部計時器允許的情況下,根據用戶移動鼠標的速度來觸發 mousemove 事件。(當然了,如果移動鼠標的速度足夠快,比如“刷”一下掃過去,瀏覽器是不會觸發這個事件的)。resize、scroll 和 key* 等事件與此類似。
可以參看這個 Demo 體會下。
Debounce
DOM 事件里的 debounce 概念其實是從機械開關和繼電器的“去彈跳”(debounce)衍生 出來的,基本思路就是把多個信號合並為一個信號。這篇文章 解釋得非常清楚,感興趣的可以一讀。
在 JavaScript 中,debounce 函數所做的事情就是,強制一個函數在某個連續時間段內只執行一次,哪怕它本來會被調用多次。我們希望在用戶停止某個操作一段時間之后才執行相應的監聽函數,而不是在用戶操作的過程當中,瀏覽器觸發多少次事件,就執行多少次監聽函數。
比如,在某個 3s 的時間段內連續地移動了鼠標,瀏覽器可能會觸發幾十(甚至幾百)個 mousemove 事件,不使用 debounce 的話,監聽函數就要執行這么多次;如果對監聽函數使用 100ms 的“去彈跳”,那么瀏覽器只會執行一次這個監聽函數,而且是在第 3.1s 的時候執行的。
現在,我們就來實現一個 debounce 函數。
實現
我們這個 debounce 函數接收兩個參數,第一個是要“去彈跳”的回調函數 fn,第二個是延遲的時間 delay。
實際上,大部分的完整
debounce實現還有第三個參數immediate,表明回調函數是在一個時間區間的最開始執行(immediate為true)還是最后執行(immediate為false),比如 underscore 的 _.debounce。本文不考慮這個參數,只考慮最后執行的情況,感興趣的可以自行研究。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * * @param fn {Function} 實際要執行的函數 * @param delay {Number} 延遲時間,也就是閾值,單位是毫秒(ms) * * @return {Function} 返回一個“去彈跳”了的函數 */ function debounce(fn, delay) { // 定時器,用來 setTimeout var timer // 返回一個函數,這個函數會在一個時間區間結束后的 delay 毫秒時執行 fn 函數 return function () { // 保存函數調用時的上下文和參數,傳遞給 fn var context = this var args = arguments // 每次這個返回的函數被調用,就清除定時器,以保證不執行 fn clearTimeout(timer) // 當返回的函數被最后一次調用后(也就是用戶停止了某個連續的操作), // 再過 delay 毫秒就執行 fn timer = setTimeout(function () { fn.apply(context, args) }, delay) } } |
其實思路很簡單,debounce 返回了一個閉包,這個閉包依然會被連續頻繁地調用,但是在閉包內部,卻限制了原始函數 fn 的執行,強制 fn 只在連續操作停止后只執行一次。
debounce 的使用方式如下:
1
2 3 |
$(document).on('mouvemove', debounce(function(e) { // 代碼 }, 250)) |
用例
還是以 mousemove 為例,為其綁定一個“去彈跳”的監聽器,效果是怎樣的?請看這個 Demo。
再來考慮另外一個場景:根據用戶的輸入實時向服務器發 ajax 請求獲取數據。我們知道,瀏覽器觸發 key* 事件也是非常快的,即便是正常人的正常打字速度,key* 事件被觸發的頻率也是很高的。以這種頻率發送請求,一是我們並沒有拿到用戶的完整輸入發送給服務器,二是這種頻繁的無用請求實在沒有必要。
更合理的處理方式是,在用戶“停止”輸入一小段時間以后,再發送請求。那么 debounce 就派上用場了:
1
2 3 |
$('input').on('keyup', debounce(function(e) { // 發送 ajax 請求 }, 300)) |
可以查看這個 Demo 看看效果。
Throttle
throttle 的概念理解起來更容易,就是固定函數執行的速率,即所謂的“節流”。正常情況下,mousemove 的監聽函數可能會每 20ms(假設)執行一次,如果設置 200ms 的“節流”,那么它就會每 200ms 執行一次。比如在 1s 的時間段內,正常的監聽函數可能會執行 50(1000/20) 次,“節流” 200ms 后則會執行 5(1000/200) 次。
我們先來看 Demo。可以看到,不管鼠標移動的速度是慢是快,“節流”后的監聽函數都會“勻速”地每 250ms 執行一次。
實現
與 debounce 類似,我們這個 throttle 也接收兩個參數,一個實際要執行的函數 fn,一個執行間隔閾值 threshhold。
同樣的,
throttle的更完整實現可以參看 underscore 的 _.throttle。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/** * * @param fn {Function} 實際要執行的函數 * @param delay {Number} 執行間隔,單位是毫秒(ms) * * @return {Function} 返回一個“節流”函數 */ function throttle(fn, threshhold) { // 記錄上次執行的時間 var last // 定時器 var timer // 默認間隔為 250ms threshhold || (threshhold = 250) // 返回的函數,每過 threshhold 毫秒就執行一次 fn 函數 return function () { // 保存函數調用時的上下文和參數,傳遞給 fn var context = this var args = arguments var now = +new Date() // 如果距離上次執行 fn 函數的時間小於 threshhold,那么就放棄 // 執行 fn,並重新計時 if (last && now < last + threshhold) { clearTimeout(timer) // 保證在當前時間區間結束后,再執行一次 fn timer = setTimeout(function () { last = now fn.apply(context, args) }, threshhold) // 在時間區間的最開始和到達指定間隔的時候執行一次 fn } else { last = now fn.apply(context, args) } } } |
原理也不復雜,相比 debounce,無非是多了一個時間間隔的判斷,其他的邏輯基本一致。throttle 的使用方式如下:
1
2 3 |
$(document).on('mouvemove', throttle(function(e) { // 代碼 }, 250)) |
用例
throttle 常用的場景是限制 resize 和 scroll 的觸發頻率。以 scroll 為例,查看這個 Demo 感受下。
可視化解釋
如果還是不能完全體會 debounce 和 throttle 的差異,可以到 這個頁面 看一下兩者可視化的比較。
總結
debounce 強制函數在某段時間內只執行一次,throttle 強制函數以固定的速率執行。在處理一些高頻率觸發的 DOM 事件的時候,它們都能極大提高用戶體驗。

