Javascript節流函式throttle和防抖函式debounce
問題的引出
在一些場景往往由於事件頻繁被觸發,因而頻繁地進行DOM操作、資源載入,導致UI停頓甚至瀏覽器崩潰。
在這樣的情況下,我們實際上的需求大多為停止改變大小n毫秒後執行後續處理;而其他事件大多的需求是以一定的頻率執行後續處理。針對這兩種需求就出現了debounce和throttle兩種解決辦法。
1. resize事件
2. mousemove事件
3. touchmove事件
4.scroll事件
throttle 與 debounce
在現在很多的javascript框架中都提供了這兩個函式。例如 jquery中有throttle和debounce外掛, underscore.js ,Lodash.js 等都提供了這兩個函式。
原理:
首先我們會想到設定一定的時間範圍delay,每隔delayms 執行不超過一次。
事件處理函式什麼時候執行能? 這裡有兩個選擇,一是先執行,再間隔delayms來等待;或者是先等待delayms,然後執行事件處理函式。
操作過程中的事件全不管,反正只執行一次事件處理。
相同低,這一次的事件處理可以是先執行一次,然後後面的事件都不管; 或者前面的都不管,最後操作完了再執行一次事件處理。
區別:
1. throttle
如果將水龍頭擰緊直到水是以水滴的形式流出,那你會發現每隔一段時間,就會有一滴水流出。
也就是會說預先設定一個執行週期,當呼叫動作的時刻大於等於執行週期則執行該動作,然後進入下一個新週期。
2.debounce
如果用手指一直按住一個彈簧,它將不會彈起直到你鬆手為止。
也就是說當呼叫動作n毫秒後,才會執行該動作,若在這n毫秒內又呼叫此動作則將重新計算執行時間。
簡單程式碼實現及實驗結果
那麼下面我們自己簡單地實現下這兩個函式:
throttle 函式:
window.addEventListener("resize",throttle(callback,300,{leading:false})); window.addEventListener("resize",callback2); function callback () { console.count("Throttled"); } function callback2 () { console.count("Not Throttled"); } /** * 頻率控制函式, fn執行次數不超過 1 次/delay * @param fn{Function} 傳入的函式 * @param delay{Number} 時間間隔 * @param options{Object} 如果想忽略開始邊界上的呼叫則傳入 {leading:false},* 如果想忽略結束邊界上的呼叫則傳入 {trailing:false},* @returns {Function} 返回呼叫函式 */ function throttle(fn,delay,options) { var wait=false; if (!options) options = {}; return function(){ var that = this,args=arguments; if(!wait){ if (!(options.leading === false)){ fn.apply(that,args); } wait=true; setTimeout(function () { if (!(options.trailing === false)){ fn.apply(that,args); } wait=false; },delay); } } }
將以上程式碼貼入瀏覽器中執行,可得到:
下面再看debounce函式的情況,
debounce 函式:
window.addEventListener("resize",{leading:false})); window.addEventListener("resize",callback2); function callback () { console.count("Throttled"); } function callback2 () { console.count("Not Throttled"); } /** * 空閒控制函式, fn僅執行一次 * @param fn{Function} 傳入的函式 * @param delay{Number} 時間間隔 * @param options{Object} 如果想忽略開始邊界上的呼叫則傳入 {leading:false},* @returns {Function} 返回呼叫函式 */ function debounce(fn,options) { var timeoutId; if (!options) options = {}; var leadingExc = false; return function() { var that = this,args = arguments; if (!leadingExc&&!(options.leading === false)) { fn.apply(that,args); } leadingExc=true; if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(function() { if (!(options.trailing === false)) { fn.apply(that,args); } leadingExc=false; },delay); } }
將以上程式碼貼入瀏覽器中執行,分三次改變視窗大小,可看到,每一次改變視窗的大小都會把開始和結束邊界的事件處理函式各執行一次:
如果是一次性改變視窗大小,會發現開始和結束的邊界各執行一次時間處理函式,請注意與一次性改變視窗大小時 throttle 情況的對比:
underscore.js 的程式碼實現
_.throttle函式
/** * 頻率控制函式, fn執行次數不超過 1 次/delay * @param fn{Function} 傳入的函式 * @param delay{Number} 時間間隔 * @param options{Object} 如果想忽略開始邊界上的呼叫則傳入 {leading:false},* @returns {Function} 返回呼叫函式 */ _.throttle = function(func,wait,options) { 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; if (remaining <= 0 || remaining > wait) { 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; }; };
_.debounce函式
/** * 空閒控制函式, fn僅執行一次 * @param fn{Function} 傳入的函式 * @param delay{Number} 時間間隔 * @param options{Object} 如果想忽略開始邊界上的呼叫則傳入 {leading:false},* @returns {Function} 返回呼叫函式 */ _.debounce = function(func,immediate) { var timeout,context,timestamp,result; var later = function() { 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(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later,wait); if (callNow) { result = func.apply(context,args); context = args = null; } return result; }; };
參考的文章
Debounce and Throttle: a visual explanation
jQuery throttle / debounce: Sometimes,less is more!
underscore.js
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。