摘要:當函數被再次觸發時,清除已設置的定時器,重新設置定時器。函數設置定時器,并根據傳參配置決定是否在等待開始時執行函數。函數取消定時器,并重置內部參數。
throttle函數與debounce函數
有時候,我們會對一些觸發頻率較高的事件進行監聽,如果在回調里執行高性能消耗的操作,反復觸發時會使得性能消耗提高,瀏覽器卡頓,用戶使用體驗差。或者我們需要對觸發的事件延遲執行回調,此時可以借助throttle/debounce函數來實現需求。
throttle函數throttle函數用于限制函數觸發的頻率,每個delay時間間隔,最多只能執行函數一次。一個最常見的例子是在監聽resize/scroll事件時,為了性能考慮,需要限制回調執行的頻率,此時便會使用throttle函數進行限制。
由throttle函數的定義可知,每個delay時間間隔,最多只能執行函數一次,所以需要有一個變量來記錄上一個執行函數的時刻,再結合延遲時間和當前觸發函數的時刻來判斷當前是否可以執行函數。在設定的時間間隔內,函數最多只能被執行一次。同時,第一次觸發時立即執行函數。以下為throttle實現的簡略代碼:
function throttle(fn, delay) { var timer; return function() { var last = timer; var now = Date.now(); if(!last) { timer = now; fn.apply(this,arguments); return; } if(last + delay > now) return; timer = now; fn.apply(this,arguments); } }debounce函數
debounce函數同樣可以減少函數觸發的頻率,但限制的方式有點不同。當函數觸發時,使用一個定時器延遲執行操作。當函數被再次觸發時,清除已設置的定時器,重新設置定時器。如果上一次的延遲操作還未執行,則會被清除。一個最常見的業務場景是監聽onchange事件,根據用戶輸入進行搜索,獲取遠程數據。為避免多次ajax請求,使用debounce函數作為onchange的回調。
由debounce的用途可知,實現延遲回調需要用到setTimeout設置定時器,每次重新觸發時需要清除原來的定時器并重新設置,簡單的代碼實現如下:
function debounce(fn, delay){ var timer; return function(){ if(timer) clearTimeout(timer) timer = setTimeout(()=>{ timer = undefined fn.apply(this, arguments); }, delay||0) } }小結
throttle函數與debounce函數的區別就是throttle函數在觸發后會馬上執行,而debounce函數會在一定延遲后才執行。從觸發開始到延遲結束,只執行函數一次。上文中throttle函數實現并未使用定時器,開源類庫提供的throttle方法大多使用定時器實現,而且開源通過參數配置項,區分throttle函數與debounce函數。
實現throttle和debounce的開源庫上文中實現的代碼較為簡單,未考慮參數類型的判斷及配置、測試等。下面介紹部分實現throttle和debounce的開源的類庫。
jQuery.throttle jQuery.debounce$.throttle指向函數jq_throttle。jq_throttle接收四個參數 delay, no_trailing, callback, debounce_mode。參數二no_trailing在throttle模式中指示。除了在文檔上說明的三個參數外,第四個參數debounce_mode用于指明是否是debounce模式,真即debounce模式,否則是throttle模式。
在jq_throttle函數內,先聲明需要使用的變量timeout_id(定時器)和last_exec(上一次執行操作的時間),進行了參數判斷和交換,然后定義了內部函數wrapper,作為返回的函數。
在wrapper內,有用于更新上次執行操作的時刻并執行真正的操作的函數exec,用于清除debounce模式中定時器的函數clear,保存當前觸發時刻和上一次執行操作時刻的時間間隔的變量elapsed。
如果是debounce模式且timeout_id空,執行exec。如果定時器timeout_id存在則清除定時器。
如果是throttle模式且elapsed大于延遲時間delay,執行exec;否則,當no_trainling非真時,更新timeout_id,重新設置定時器,補充在上面清除的定時器:如果是debounce模式,執行timeout_id = setTimeout(clear, delay),如果是throttle模式,執行timeout_id = setTimeout(exec, delay - elapsed)。
$.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) { // After wrapper has stopped being called, this timeout ensures that // `callback` is executed at the proper times in `throttle` and `end` // debounce modes. var timeout_id, // Keep track of the last time `callback` was executed. last_exec = 0; // `no_trailing` defaults to falsy. if ( typeof no_trailing !== "boolean" ) { debounce_mode = callback; callback = no_trailing; no_trailing = undefined; } // The `wrapper` function encapsulates all of the throttling / debouncing // functionality and when executed will limit the rate at which `callback` // is executed. function wrapper() { var that = this, elapsed = +new Date() - last_exec, args = arguments; // Execute `callback` and update the `last_exec` timestamp. function exec() { last_exec = +new Date(); callback.apply( that, args ); }; // If `debounce_mode` is true (at_begin) this is used to clear the flag // to allow future `callback` executions. function clear() { timeout_id = undefined; }; if ( debounce_mode && !timeout_id ) { // Since `wrapper` is being called for the first time and // `debounce_mode` is true (at_begin), execute `callback`. exec(); } // Clear any existing timeout. timeout_id && clearTimeout( timeout_id ); if ( debounce_mode === undefined && elapsed > delay ) { // In throttle mode, if `delay` time has been exceeded, execute // `callback`. exec(); } else if ( no_trailing !== true ) { // In trailing throttle mode, since `delay` time has not been // exceeded, schedule `callback` to execute `delay` ms after most // recent execution. // // If `debounce_mode` is true (at_begin), schedule `clear` to execute // after `delay` ms. // // If `debounce_mode` is false (at end), schedule `callback` to // execute after `delay` ms. timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay ); } }; // Set the guid of `wrapper` function to the same of original callback, so // it can be removed in jQuery 1.4+ .unbind or .die by using the original // callback as a reference. if ( $.guid ) { wrapper.guid = callback.guid = callback.guid || $.guid++; } // Return the wrapper function. return wrapper; };
debounce函數內部實際調用了throttle函數。
$.debounce = function( delay, at_begin, callback ) { return callback === undefined ? jq_throttle( delay, at_begin, false ) : jq_throttle( delay, callback, at_begin !== false ); };lodash的throttle與debounce
lodash中相比jQuery,提供了leading和trailing選項,表示在函數在等待開始時被執行和函數在等待結束時被執行。而對于debounce函數,還提供了maxWait,當debounce函數重復觸發時,有可能由于wait過長,回調函數沒機會執行,maxWait字段確保了當函數重復觸發時,每maxWait毫秒執行函數一次。
由maxWait的作用,我們可以聯想到,提供maxWait的debounce函數與throttle函數的作用是一樣的;事實上,lodash的throttle函數就是指明maxWait的debounce函數。
lodash重新設置計時器時,并沒有調用clearTimeout清除定時器,而是在執行回調前判斷參數和執行上下文是否存在,存在時則執行回調,執行完之后將參數和上下文賦值為undefined;重復觸發時,參數和上下文為空,不執行函數。這也是與jQuery實現的不同之一
以下為debounce函數內的函數和變量:
局部變量lastInvokeTime記錄上次執行時間,默認0。
函數invokeFunc執行回調操作,并更新上一次執行時間lastInvokeTime。
函數leadingEdge設置定時器,并根據傳參配置決定是否在等待開始時執行函數。
函數shouldInvoke判斷是否可以執行回調函數。
函數timerExpired判斷是否可以立即執行函數,如果可以則執行,否則重新設置定時器,函數remainingWait根據上次觸發時間/執行時間和當前時間返回重新設置的定時器的時間間隔。
函數trailingEdge根據配置決定是否執行函數,并清空timerId。
函數cancel取消定時器,并重置內部參數。函數debounced是返回的內部函數。
debounced內部先獲取當前時間time,判斷是否能執行函數。如果可以執行,且timerId空,表示可以馬上執行函數(說明是第一次觸發或已經執行過trailingEdge),執行leadingEdge,設置定時器。
如果timerId非空且傳參選項有maxWait,說明是throttle函數,設置定時器延遲執行timerExpired并立即執行invokeFunc,此時在timerExpired中設置的定時器的延遲執行時間是wait - timeSinceLastCall與maxWait - timeSinceLastInvoke的最小值,分別表示通過wait設置的仍需等待執行函數的時間(下一次trailing的時間)和通過maxWait設置的仍需等待執行函數的時間(下一次maxing的時間)。
function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; if (typeof func != "function") { throw new TypeError(FUNC_ERROR_TEXT); } wait = toNumber(wait) || 0; if (isObject(options)) { leading = !!options.leading; maxing = "maxWait" in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; trailing = "trailing" in options ? !!options.trailing : trailing; } function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we"re at the // trailing edge, the system time has gone backwards and we"re treating // it as the trailing edge, or we"ve hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now()); } function debounced() { var time = now(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; }
throttle函數則是設置了maxWait選項且leading為真的debounce函數。
function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != "function") { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { "leading": leading, "maxWait": wait, "trailing": trailing }); }參考
Throttling and debouncing in JavaScript
Debouncing and Throttling Explained Through Examples
jquery-throttle-debounce源碼
_.debounce源碼
聊聊lodash的debounce實現
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108445.html
摘要:淺談以及的原理和實現背景日常開發中我們經常會遇到一些需要節流調用或者壓縮調用次數的情況例如之前我在完成一個需求的時候就遇到了因為后端并發問題導致收到多條信息從而導致函數被重復調用的情況當時的做法是通過對函數的調用進行注冊遇到多次調用的時候清 淺談throttle以及debounce的原理和實現 背景 日常開發中,我們經常會遇到一些需要節流調用,或者壓縮調用次數的情況,例如之前我在完成...
摘要:可以看下面的栗子這個圖中圖中每個小格大約,右邊有原生事件與節流去抖插件的與事件。即如果有連續不斷的觸發,每執行一次,用在每隔一定間隔執行回調的場景。執行啦打印執行啦打印執行啦節流按照上面的說明,節流就是連續多次內的操作按照指定的間隔來執行。 一般在項目中我們會對input、scroll、resize等事件進行節流控制,防止事件過多觸發,減少資源消耗;在vue的官網的例子中就有關于lod...
摘要:那么還有最后一個問題,那我之前設置的定時器怎么辦呢定時器執行的是這個函數,而這個函數又會通過進行一次判斷。 我們在處理事件的時候,有些事件由于觸發太頻繁,而每次事件都處理的話,會消耗太多資源,導致瀏覽器崩潰。最常見的是我們在移動端實現無限加載的時候,移動端本來滾動就不是很靈敏,如果每次滾動都處理的話,界面就直接卡死了。 因此,我們通常會選擇,不立即處理事件,而是在觸發一定次數或一定時間...
摘要:一個使用場景某些瀏覽器事件可能會在短時間內高頻觸發,比如整窗口大小或滾動頁面。這會導致非常嚴重的性能問題。實現與類似,接收兩個參數,一個是需要截流的函數,另一個是函數執行間隔閾值。 一個使用場景:某些瀏覽器事件可能會在短時間內高頻觸發,比如:整窗口大小或滾動頁面。如果給窗口滾動事件添加一個事件監聽器,然后用戶不停地快速滾動頁面,那你的事件可能在短短數秒之內被觸發數千次。這會導致非常嚴重...
摘要:譯通過實例講解和防抖與節流源碼中推薦的文章,為了學習英語,翻譯了一下原文鏈接作者本文來自一位倫敦前端工程師的技術投稿。首次或立即你可能發現防抖事件在等待觸發事件執行,直到事件都結束后它才執行。 [譯]通過實例講解Debouncing和Throtting(防抖與節流) lodash源碼中推薦的文章,為了學習(英語),翻譯了一下~ 原文鏈接 作者:DAVID CORBACHO 本文來自一位...
閱讀 3247·2021-09-22 15:58
閱讀 1715·2019-08-30 14:17
閱讀 1716·2019-08-28 18:05
閱讀 1504·2019-08-26 13:33
閱讀 683·2019-08-26 12:20
閱讀 606·2019-08-26 12:18
閱讀 3192·2019-08-26 11:59
閱讀 1400·2019-08-26 10:36