在處理諸如 resize、scroll、mousemove 和 keydown/keyup/keypress 等事件的時候,通常我們不希望這些事件太過頻繁地觸發,尤其是監聽程式中涉及到大量的計算或者有非常耗費資源的操作。 有多頻繁呢?以 mousemove 為例,根據 DOM Level 3 的規定 ...
在處理諸如 resize
、scroll
、mousemove
和 keydown/keyup/keypress
等事件的時候,通常我們不希望這些事件太過頻繁地觸發,尤其是監聽程式中涉及到大量的計算或者有非常耗費資源的操作。
有多頻繁呢?以 mousemove
為例,根據 DOM Level 3 的規定,「如果滑鼠連續移動,那麼瀏覽器就應該觸發多個連續的 mousemove
事件」,這意味著瀏覽器會在其內部計時器允許的情況下,根據用戶移動滑鼠的速度來觸發 mousemove
事件。(當然了,如果移動滑鼠的速度足夠快,比如“刷”一下掃過去,瀏覽器是不會觸發這個事件的)。resize
、scroll
和 key*
等事件與此類似。
Debounce
DOM 事件里的 debounce
概念其實是從機械開關和繼電器的“去彈跳”(debounce)衍生 出來的,基本思路就是把多個信號合併為一個信號。這篇文章 解釋得非常清楚,感興趣的可以一讀。
在 JavaScript 中,debounce
函數所做的事情就是,強制一個函數在某個連續時間段內只執行一次,哪怕它本來會被調用多次。我們希望在用戶停止某個操作一段時間之後才執行相應的監聽函數,而不是在用戶操作的過程當中,瀏覽器觸發多少次事件,就執行多少次監聽函數。
比如,在某個 3s 的時間段內連續地移動了滑鼠,瀏覽器可能會觸發幾十(甚至幾百)個 mousemove
事件,不使用 debounce
的話,監聽函數就要執行這麼多次;如果對監聽函數使用 100ms 的“去彈跳”,那麼瀏覽器只會執行一次這個監聽函數,而且是在第 3.1s 的時候執行的。
現在,我們就來實現一個 debounce
函數。
實現
我們這個 debounce
函數接收兩個參數,第一個是要“去彈跳”的回調函數 fn
,第二個是延遲的時間 delay
。
實際上,大部分的完整
debounce
實現還有第三個參數immediate
,表明回調函數是在一個時間區間的最開始執行(immediate
為true
)還是最後執行(immediate
為false
),比如 underscore 的 _.debounce。本文不考慮這個參數,只考慮最後執行的情況,感興趣的可以自行研究。
/** * * @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
的使用方式如下:其實思路很簡單,debounce
返回了一個閉包,這個閉包依然會被連續頻繁地調用,但是在閉包內部,卻限制了原始函數 fn
的執行,強制 fn
只在連續操作停止後只執行一次。
$(document).on('mouvemove', debounce(function(e) { // 代碼 }, 250))
還是以 mousemove
為例,為其綁定一個“去彈跳”的監聽器,效果是怎樣的?請看這個 Demo。用例
再來考慮另外一個場景:根據用戶的輸入實時向伺服器發 ajax 請求獲取數據。我們知道,瀏覽器觸發 key*
事件也是非常快的,即便是正常人的正常打字速度,key*
事件被觸發的頻率也是很高的。以這種頻率發送請求,一是我們並沒有拿到用戶的完整輸入發送給伺服器,二是這種頻繁的無用請求實在沒有必要。
更合理的處理方式是,在用戶“停止”輸入一小段時間以後,再發送請求。那麼 debounce
就派上用場了:
$('input').on('keyup', debounce(function(e) { // 發送 ajax 請求 }, 300))
Throttle
throttle
的概念理解起來更容易,就是固定函數執行的速率,即所謂的“節流”。正常情況下,mousemove
的監聽函數可能會每 20ms(假設)執行一次,如果設置 200ms 的“節流”,那麼它就會每 200ms 執行一次。比如在 1s 的時間段內,正常的監聽函數可能會執行 50(1000/20) 次,“節流” 200ms 後則會執行 5(1000/200) 次。
我們先來看 Demo。可以看到,不管滑鼠移動的速度是慢是快,“節流”後的監聽函數都會“勻速”地每 250ms 執行一次。
實現
與 debounce
類似,我們這個 throttle
也接收兩個參數,一個實際要執行的函數 fn
,一個執行間隔閾值 threshhold
。
同樣的,
throttle
的更完整實現可以參看 underscore 的 _.throttle。
/** * * @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
的使用方式如下:
$(document).on('mouvemove', throttle(function(e) { // 代碼 }, 250))
用例
throttle
常用的場景是限制 resize
和 scroll
的觸發頻率。以 scroll
為例。
總結
debounce
強制函數在某段時間內只執行一次,throttle
強制函數以固定的速率執行。在處理一些高頻率觸發的 DOM 事件的時候,它們都能極大提高用戶體驗。