在處理諸如 resize
、scroll
、mousemove
和 keydown/keyup/keypress
等事件的時候,一般咱們不但願這些事件太過頻繁地觸發,尤爲是監聽程序中涉及到大量的計算或者有很是耗費資源的操做。javascript
有多頻繁呢?以 mousemove
爲例,根據 DOM Level 3 的規定,「若是鼠標連續移動,那麼瀏覽器就應該觸發多個連續的 mousemove
事件」,這意味着瀏覽器會在其內部計時器容許的狀況下,根據用戶移動鼠標的速度來觸發 mousemove
事件。(固然了,若是移動鼠標的速度足夠快,好比「刷」一下掃過去,瀏覽器是不會觸發這個事件的)。resize
、scroll
和 key*
等事件與此相似。java
能夠參看這個 Demo 體會下。ajax
DOM 事件裏的 debounce
概念實際上是從機械開關和繼電器的「去彈跳」(debounce)衍生 出來的,基本思路就是把多個信號合併爲一個信號。這篇文章 解釋得很是清楚,感興趣的能夠一讀。瀏覽器
在 JavaScript 中,debounce
函數所作的事情就是,強制一個函數在某個連續時間段內只執行一次,哪怕它原本會被調用屢次。咱們但願在用戶中止某個操做一段時間以後才執行相應的監聽函數,而不是在用戶操做的過程中,瀏覽器觸發多少次事件,就執行多少次監聽函數。服務器
好比,在某個 3s 的時間段內連續地移動了鼠標,瀏覽器可能會觸發幾十(甚至幾百)個 mousemove
事件,不使用 debounce
的話,監聽函數就要執行這麼屢次;若是對監聽函數使用 100ms 的「去彈跳」,那麼瀏覽器只會執行一次這個監聽函數,並且是在第 3.1s 的時候執行的。閉包
如今,咱們就來實現一個 debounce
函數。app
咱們這個 debounce
函數接收兩個參數,第一個是要「去彈跳」的回調函數 fn
,第二個是延遲的時間 delay
。函數
實際上,大部分的完整
debounce
實現還有第三個參數immediate
,代表回調函數是在一個時間區間的最開始執行(immediate
爲true
)仍是最後執行(immediate
爲false
),好比 underscore 的 _.debounce。本文不考慮這個參數,只考慮最後執行的狀況,感興趣的能夠自行研究。this
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
只在連續操做中止後只執行一次。spa
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
的概念理解起來更容易,就是固定函數執行的速率,即所謂的「節流」。正常狀況下,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 事件的時候,它們都能極大提升用戶體驗。