一、前言
以下場景往往由于事件頻繁被觸發,因而頻繁執行DOM操作、資源加載等重行為,導致UI停頓甚至瀏覽器崩潰。
1. window對象的resize、scroll事件
2. 拖拽時的mousemove事件
3. 射擊游戲中的mousedown、keydown事件
4. 文字輸入、自動完成的keyup事件
實際上對于window的resize事件,實際需求大多為停止改變大小n毫秒后執行后續處理;而其他事件大多的需求是以一定的頻率執行后續處理。針對這兩種需求就出現了debounce和throttle兩種解決辦法。
二、什么是debounce
1. 定義
如果用手指一直按住一個彈簧,它將不會彈起直到你松手為止。
也就是說當調用動作n毫秒后,才會執行該動作,若在這n毫秒內又調用此動作則將重新計算執行時間。
接口定義:
/** * 空閑控制 返回函數連續調用時,空閑時間必須大于或等于 idle,action 才會執行 * @param idle {number} 空閑時間,單位毫秒 * @param action {function} 請求關聯函數,實際應用需要調用的函數 * @return {function} 返回客戶調用函數 */ debounce(idle,action)
2. 簡單實現
var debounce = function(idle, action){ var last return function(){ var ctx = this, args = arguments clearTimeout(last) last = setTimeout(function(){ action.apply(ctx, args) }, idle) } }
三、什么是throttle
1. 定義
如果將水龍頭擰緊直到水是以水滴的形式流出,那你會發現每隔一段時間,就會有一滴水流出。
也就是會說預先設定一個執行周期,當調用動作的時刻大于等于執行周期則執行該動作,然后進入下一個新周期。
接口定義:
/** * 頻率控制 返回函數連續調用時,action 執行頻率限定為 次 / delay * @param delay {number} 延遲時間,單位毫秒 * @param action {function} 請求關聯函數,實際應用需要調用的函數 * @return {function} 返回客戶調用函數 */ throttle(delay,action)
2. 簡單實現
var throttle = function(delay, action){ var last = 0return function(){ var curr = +new Date() if (curr - last > delay){ action.apply(this, arguments)
last = curr
} } }
四、underscore v1.7.0相關的源碼剖析
1. _.throttle函數
_.throttle = function(func, wait, options) { /* options的默認值 * 表示首次調用返回值方法時,會馬上調用func;否則僅會記錄當前時刻,當第二次調用的時間間隔超過wait時,才調用func。 * options.leading = true; * 表示當調用方法時,未到達wait指定的時間間隔,則啟動計時器延遲調用func函數,若后續在既未達到wait指定的時間間隔和func函數又未被調用的情況下調用返回值方法,則被調用請求將被丟棄。 * options.trailing = true; * 注意:當options.trailing = false時,效果與上面的簡單實現效果相同 */ var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : _.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _.now(); if (!previous && options.leading === false) previous = now; // 計算剩余時間 var remaining = wait - (now - previous); context = this; args = arguments; // 當到達wait指定的時間間隔,則調用func函數 // 精彩之處:按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,但這里還考慮到假如客戶端修改了系統時間則馬上執行func函數。 if (remaining <= 0 || remaining > wait) { // 由于setTimeout存在最小時間精度問題,因此會存在到達wait的時間間隔,但之前設置的setTimeout操作還沒被執行,因此為保險起見,這里先清理setTimeout操作 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // options.trailing=true時,延時執行func函數 timeout = setTimeout(later, remaining); } return result; }; };
按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,至于remaining > wait的作用是什么,我現在也不太清楚。
精彩之處:按理來說remaining <= 0已經足夠證明已經到達wait的時間間隔,但這里還考慮到假如客戶端修改了系統時間則馬上執行func函數。這里謝謝@GreatFeng的提示!
2. _.debounce函數
_.debounce = function(func, wait, immediate) { // immediate默認為false var timeout, args, context, timestamp, result; var later = function() { // 當wait指定的時間間隔期間多次調用_.debounce返回的函數,則會不斷更新timestamp的值,導致last < wait && last >= 0一直為true,從而不斷啟動新的計時器延時執行func var last = _.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; timestamp = _.now(); // 第一次調用該方法時,且immediate為true,則調用func函數 var callNow = immediate && !timeout; // 在wait指定的時間間隔內首次調用該方法,則啟動計時器定時調用func函數 if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; };
_.debounce實現的精彩之處我認為是通過遞歸啟動計時器來代替通過調用clearTimeout來調整調用func函數的延時執行。
五、總結
throttle和debounce均是通過減少實際邏輯處理過程的執行來提高事件處理函數運行性能的手段,并沒有實質上減少事件的觸發次數。兩者在概念理解上確實比較容易令人混淆,結合各js庫的具體實現進行理解效果將會更好。
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4147810.html ^_^肥子John
六、參考
http://www.alloyteam.com/2012/11/javascript-throttle/
http://www.cnblogs.com/ambar/archive/2011/10/08/throttle-and-debounce.html
文章列表