做爲一名前端開發者,咱們常常會處理各類事件,好比常見的click、scroll、 resize等等。仔細一想,會發現像scroll、onchange這類事件會頻繁觸發,若是咱們在回調中計算元素位置、作一些跟DOM相關的操做,引發瀏覽器迴流和重繪,頻繁觸發回調,極可能會形成瀏覽器掉幀,甚至會使瀏覽器崩潰,影響用戶體驗。針對這種現象,目前有兩種經常使用的解決方案:防抖和節流。html
所謂防抖,就是指觸發事件後,就是把觸發很是頻繁的事件合併成一次去執行。即在指定時間內只執行一次回調函數,若是在指定的時間內又觸發了該事件,則回調函數的執行時間會基於此刻從新開始計算。前端
以咱們生活中乘車刷卡的情景舉例,只要乘客不斷地在刷卡,司機師傅就不能開車,乘客刷卡完畢以後,司機會等待幾分鐘,肯定乘客坐穩再開車。若是司機在最後等待的時間內又有新的乘客上車,那麼司機等乘客刷卡完畢以後,還要再等待一會,等待全部乘客坐穩再開車。html5
具體應該怎麼去實現這樣的功能呢?第一時間確定會想到使用setTimeout方法,那咱們就嘗試寫一個簡單的函數來實現這個功能吧~瀏覽器
var debounce = function(fn, delayTime) { var timeId; return function () { var context = this, args = arguments; timeId && clearTimeout(timeout); timeId = setTimeout(function { fn.apply(context, args); }, delayTime) } }
思路解析:閉包
執行debounce函數以後會返回一個新的函數,經過閉包的形式,維護一個變量timeId,每次執行該函數的時候會結束以前的延遲操做,從新執行setTimeout方法,也就實現了上面所說的指定的時間內屢次觸發同一個事件,會合並執行一次。app
舒適提示:函數
一、上述代碼中arguments只會保存事件回調函數中的參數,譬如:事件對象等,並不會保存fn、delayTime工具
二、使用apply改變傳入的fn方法中的this指向,指向綁定事件的DOM元素。性能
所謂節流,是指頻繁觸發事件時,只會在指定的時間段內執行事件回調,即觸發事件間隔大於等於指定的時間纔會執行回調函數。學習
類比到生活中的水龍頭,擰緊水龍頭到某種程度會發現,每隔一段時間,就會有水滴流出。
說到時間間隔,你們確定會想到使用setTimeout來實現,在這裏,咱們使用兩種方法來簡單實現這種功能:時間戳和setTimeout定時器。
var throttle = (fn, delayTime) => { var _start = Date.now(); return function () { var _now = Date.now(), context = this, args = arguments; if(_now - _start >= delayTime) { fn.apply(context, args); _start = Date.now(); } } }
經過比較兩次時間戳的間隔是否大於等於咱們事先指定的時間來決定是否執行事件回調。
var throttle = function (fn, delayTime) { var flag; return function () { var context = this, args = arguments; if(!flag) { flag = setTimeout(function () { fn.apply(context, args); flag = false; }, delayTime); } } }
在上述實現過程當中,咱們設置了一個標誌變量flag,當delayTime以後執行事件回調,便會把這個變量重置,表示一次回調已經執行結束。
對比上述兩種實現,咱們會發現一個有趣的現象:
一、使用時間戳方式,頁面加載的時候就會開始計時,若是頁面加載時間大於咱們設定的delayTime,第一次觸發事件回調的時候便會當即fn,並不會延遲。若是最後一次觸發回調與前一次觸發回調的時間差小於delayTime,則最後一次觸發事件並不會執行fn;
二、使用定時器方式,咱們第一次觸發回調的時候纔會開始計時,若是最後一次觸發回調事件與前一次時間間隔小於delayTime,delayTime以後仍會執行fn。
這兩種方式有點優點互補的意思,哈哈~
咱們考慮把這兩種方式結合起來,便會在第一次觸發事件時執行fn,最後一次與前一次間隔比較短,delayTime以後再次執行fn。
想法簡單實現以下:
var throttle = function (fn, delayTime) { var flag, _start = Date.now(); return function () { var context = this, args = arguments, _now = Date.now(), remainTime = delayTime - (_now - _start); if(remainTime <= 0) { fn.apply(this, args); } else { setTimeout(function () { fn.apply(this, args); }, remainTime) } } }
經過上面的分析,能夠很明顯的看出函數防抖和函數節流的區別:
頻繁觸發事件時,函數防抖只會在最後一次觸發事件只會纔會執行回調內容,其餘狀況下會從新計算延遲事件,而函數節流便會頗有規律的每隔必定時間執行一次回調函數。
以前,咱們使用setTimeout簡單實現了防抖和節流功能,若是咱們不考慮兼容性,追求精度比較高的頁面效果,能夠考慮試試html5提供的API--requestAnimationFrame。
與setTimeout相比,requestAnimationFrame的時間間隔是有系統來決定,保證屏幕刷新一次,回調函數只會執行一次,好比屏幕的刷新頻率是60HZ,即間隔1000ms/60會執行一次回調。
var throttle = function(fn, delayTime) { var flag; return function() { if(!flag) { requestAnimationFrame(function() { fn(); flag = false; }); flag = true; } }
上述代碼的基本功能就是保證在屏幕刷新的時候(對於大多數的屏幕來講,大約16.67ms),能夠執行一次回調函數fn。使用這種方式也存在一種比較明顯的缺點,時間間隔只能跟隨系統變化,咱們沒法修改,可是準確性會比setTimeout高一些。
注意:
防抖和節流只是減小了事件回調函數的執行次數,並不會減小事件的觸發頻率。
防抖和節流並無從本質上解決性能問題,咱們還應該注意優化咱們事件回調函數的邏輯功能,避免在回調中執行比較複雜的DOM操做,減小瀏覽器reflow和repaint。
上面的示例代碼比較簡單,只是說明了基本的思路。目前已經有工具庫實現了這些功能,好比underscore,考慮的狀況也會比較多,你們能夠去查看源碼,學習做者的思路,加深理解。
underscore的debounce方法源碼:
_.debounce = function(func, wait, immediate) { var timeout, result; var later = function(context, args) { timeout = null; if (args) result = func.apply(context, args); }; var debounced = restArguments(function(args) { if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(later, wait); if (callNow) result = func.apply(this, args); } else { timeout = _.delay(later, wait, this, args); } return result; }); debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; };
underscore的throttle源碼:
_.throttle = function(func, wait, options) { var timeout, context, args, result; 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; }; var throttled = function() { var now = _.now(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; };