摘要:當函數(shù)被再次觸發(fā)時,清除已設(shè)置的定時器,重新設(shè)置定時器。函數(shù)設(shè)置定時器,并根據(jù)傳參配置決定是否在等待開始時執(zhí)行函數(shù)。函數(shù)取消定時器,并重置內(nèi)部參數(shù)。
throttle函數(shù)與debounce函數(shù)
有時候,我們會對一些觸發(fā)頻率較高的事件進行監(jiān)聽,如果在回調(diào)里執(zhí)行高性能消耗的操作,反復(fù)觸發(fā)時會使得性能消耗提高,瀏覽器卡頓,用戶使用體驗差?;蛘呶覀冃枰獙τ|發(fā)的事件延遲執(zhí)行回調(diào),此時可以借助throttle/debounce函數(shù)來實現(xiàn)需求。
throttle函數(shù)throttle函數(shù)用于限制函數(shù)觸發(fā)的頻率,每個delay時間間隔,最多只能執(zhí)行函數(shù)一次。一個最常見的例子是在監(jiān)聽resize/scroll事件時,為了性能考慮,需要限制回調(diào)執(zhí)行的頻率,此時便會使用throttle函數(shù)進行限制。
由throttle函數(shù)的定義可知,每個delay時間間隔,最多只能執(zhí)行函數(shù)一次,所以需要有一個變量來記錄上一個執(zhí)行函數(shù)的時刻,再結(jié)合延遲時間和當前觸發(fā)函數(shù)的時刻來判斷當前是否可以執(zhí)行函數(shù)。在設(shè)定的時間間隔內(nèi),函數(shù)最多只能被執(zhí)行一次。同時,第一次觸發(fā)時立即執(zhí)行函數(shù)。以下為throttle實現(xiàn)的簡略代碼:
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函數(shù)
debounce函數(shù)同樣可以減少函數(shù)觸發(fā)的頻率,但限制的方式有點不同。當函數(shù)觸發(fā)時,使用一個定時器延遲執(zhí)行操作。當函數(shù)被再次觸發(fā)時,清除已設(shè)置的定時器,重新設(shè)置定時器。如果上一次的延遲操作還未執(zhí)行,則會被清除。一個最常見的業(yè)務(wù)場景是監(jiān)聽onchange事件,根據(jù)用戶輸入進行搜索,獲取遠程數(shù)據(jù)。為避免多次ajax請求,使用debounce函數(shù)作為onchange的回調(diào)。
由debounce的用途可知,實現(xiàn)延遲回調(diào)需要用到setTimeout設(shè)置定時器,每次重新觸發(fā)時需要清除原來的定時器并重新設(shè)置,簡單的代碼實現(xiàn)如下:
function debounce(fn, delay){ var timer; return function(){ if(timer) clearTimeout(timer) timer = setTimeout(()=>{ timer = undefined fn.apply(this, arguments); }, delay||0) } }小結(jié)
throttle函數(shù)與debounce函數(shù)的區(qū)別就是throttle函數(shù)在觸發(fā)后會馬上執(zhí)行,而debounce函數(shù)會在一定延遲后才執(zhí)行。從觸發(fā)開始到延遲結(jié)束,只執(zhí)行函數(shù)一次。上文中throttle函數(shù)實現(xiàn)并未使用定時器,開源類庫提供的throttle方法大多使用定時器實現(xiàn),而且開源通過參數(shù)配置項,區(qū)分throttle函數(shù)與debounce函數(shù)。
實現(xiàn)throttle和debounce的開源庫上文中實現(xiàn)的代碼較為簡單,未考慮參數(shù)類型的判斷及配置、測試等。下面介紹部分實現(xiàn)throttle和debounce的開源的類庫。
jQuery.throttle jQuery.debounce$.throttle指向函數(shù)jq_throttle。jq_throttle接收四個參數(shù) delay, no_trailing, callback, debounce_mode。參數(shù)二no_trailing在throttle模式中指示。除了在文檔上說明的三個參數(shù)外,第四個參數(shù)debounce_mode用于指明是否是debounce模式,真即debounce模式,否則是throttle模式。
在jq_throttle函數(shù)內(nèi),先聲明需要使用的變量timeout_id(定時器)和last_exec(上一次執(zhí)行操作的時間),進行了參數(shù)判斷和交換,然后定義了內(nèi)部函數(shù)wrapper,作為返回的函數(shù)。
在wrapper內(nèi),有用于更新上次執(zhí)行操作的時刻并執(zhí)行真正的操作的函數(shù)exec,用于清除debounce模式中定時器的函數(shù)clear,保存當前觸發(fā)時刻和上一次執(zhí)行操作時刻的時間間隔的變量elapsed。
如果是debounce模式且timeout_id空,執(zhí)行exec。如果定時器timeout_id存在則清除定時器。
如果是throttle模式且elapsed大于延遲時間delay,執(zhí)行exec;否則,當no_trainling非真時,更新timeout_id,重新設(shè)置定時器,補充在上面清除的定時器:如果是debounce模式,執(zhí)行timeout_id = setTimeout(clear, delay),如果是throttle模式,執(zhí)行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函數(shù)內(nèi)部實際調(diào)用了throttle函數(shù)。
$.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選項,表示在函數(shù)在等待開始時被執(zhí)行和函數(shù)在等待結(jié)束時被執(zhí)行。而對于debounce函數(shù),還提供了maxWait,當debounce函數(shù)重復(fù)觸發(fā)時,有可能由于wait過長,回調(diào)函數(shù)沒機會執(zhí)行,maxWait字段確保了當函數(shù)重復(fù)觸發(fā)時,每maxWait毫秒執(zhí)行函數(shù)一次。
由maxWait的作用,我們可以聯(lián)想到,提供maxWait的debounce函數(shù)與throttle函數(shù)的作用是一樣的;事實上,lodash的throttle函數(shù)就是指明maxWait的debounce函數(shù)。
lodash重新設(shè)置計時器時,并沒有調(diào)用clearTimeout清除定時器,而是在執(zhí)行回調(diào)前判斷參數(shù)和執(zhí)行上下文是否存在,存在時則執(zhí)行回調(diào),執(zhí)行完之后將參數(shù)和上下文賦值為undefined;重復(fù)觸發(fā)時,參數(shù)和上下文為空,不執(zhí)行函數(shù)。這也是與jQuery實現(xiàn)的不同之一
以下為debounce函數(shù)內(nèi)的函數(shù)和變量:
局部變量lastInvokeTime記錄上次執(zhí)行時間,默認0。
函數(shù)invokeFunc執(zhí)行回調(diào)操作,并更新上一次執(zhí)行時間lastInvokeTime。
函數(shù)leadingEdge設(shè)置定時器,并根據(jù)傳參配置決定是否在等待開始時執(zhí)行函數(shù)。
函數(shù)shouldInvoke判斷是否可以執(zhí)行回調(diào)函數(shù)。
函數(shù)timerExpired判斷是否可以立即執(zhí)行函數(shù),如果可以則執(zhí)行,否則重新設(shè)置定時器,函數(shù)remainingWait根據(jù)上次觸發(fā)時間/執(zhí)行時間和當前時間返回重新設(shè)置的定時器的時間間隔。
函數(shù)trailingEdge根據(jù)配置決定是否執(zhí)行函數(shù),并清空timerId。
函數(shù)cancel取消定時器,并重置內(nèi)部參數(shù)。函數(shù)debounced是返回的內(nèi)部函數(shù)。
debounced內(nèi)部先獲取當前時間time,判斷是否能執(zhí)行函數(shù)。如果可以執(zhí)行,且timerId空,表示可以馬上執(zhí)行函數(shù)(說明是第一次觸發(fā)或已經(jīng)執(zhí)行過trailingEdge),執(zhí)行leadingEdge,設(shè)置定時器。
如果timerId非空且傳參選項有maxWait,說明是throttle函數(shù),設(shè)置定時器延遲執(zhí)行timerExpired并立即執(zhí)行invokeFunc,此時在timerExpired中設(shè)置的定時器的延遲執(zhí)行時間是wait - timeSinceLastCall與maxWait - timeSinceLastInvoke的最小值,分別表示通過wait設(shè)置的仍需等待執(zhí)行函數(shù)的時間(下一次trailing的時間)和通過maxWait設(shè)置的仍需等待執(zhí)行函數(shù)的時間(下一次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函數(shù)則是設(shè)置了maxWait選項且leading為真的debounce函數(shù)。
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實現(xiàn)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108445.html
摘要:淺談以及的原理和實現(xiàn)背景日常開發(fā)中我們經(jīng)常會遇到一些需要節(jié)流調(diào)用或者壓縮調(diào)用次數(shù)的情況例如之前我在完成一個需求的時候就遇到了因為后端并發(fā)問題導(dǎo)致收到多條信息從而導(dǎo)致函數(shù)被重復(fù)調(diào)用的情況當時的做法是通過對函數(shù)的調(diào)用進行注冊遇到多次調(diào)用的時候清 淺談throttle以及debounce的原理和實現(xiàn) 背景 日常開發(fā)中,我們經(jīng)常會遇到一些需要節(jié)流調(diào)用,或者壓縮調(diào)用次數(shù)的情況,例如之前我在完成...
摘要:可以看下面的栗子這個圖中圖中每個小格大約,右邊有原生事件與節(jié)流去抖插件的與事件。即如果有連續(xù)不斷的觸發(fā),每執(zhí)行一次,用在每隔一定間隔執(zhí)行回調(diào)的場景。執(zhí)行啦打印執(zhí)行啦打印執(zhí)行啦節(jié)流按照上面的說明,節(jié)流就是連續(xù)多次內(nèi)的操作按照指定的間隔來執(zhí)行。 一般在項目中我們會對input、scroll、resize等事件進行節(jié)流控制,防止事件過多觸發(fā),減少資源消耗;在vue的官網(wǎng)的例子中就有關(guān)于lod...
摘要:那么還有最后一個問題,那我之前設(shè)置的定時器怎么辦呢定時器執(zhí)行的是這個函數(shù),而這個函數(shù)又會通過進行一次判斷。 我們在處理事件的時候,有些事件由于觸發(fā)太頻繁,而每次事件都處理的話,會消耗太多資源,導(dǎo)致瀏覽器崩潰。最常見的是我們在移動端實現(xiàn)無限加載的時候,移動端本來滾動就不是很靈敏,如果每次滾動都處理的話,界面就直接卡死了。 因此,我們通常會選擇,不立即處理事件,而是在觸發(fā)一定次數(shù)或一定時間...
摘要:一個使用場景某些瀏覽器事件可能會在短時間內(nèi)高頻觸發(fā),比如整窗口大小或滾動頁面。這會導(dǎo)致非常嚴重的性能問題。實現(xiàn)與類似,接收兩個參數(shù),一個是需要截流的函數(shù),另一個是函數(shù)執(zhí)行間隔閾值。 一個使用場景:某些瀏覽器事件可能會在短時間內(nèi)高頻觸發(fā),比如:整窗口大小或滾動頁面。如果給窗口滾動事件添加一個事件監(jiān)聽器,然后用戶不停地快速滾動頁面,那你的事件可能在短短數(shù)秒之內(nèi)被觸發(fā)數(shù)千次。這會導(dǎo)致非常嚴重...
摘要:譯通過實例講解和防抖與節(jié)流源碼中推薦的文章,為了學習英語,翻譯了一下原文鏈接作者本文來自一位倫敦前端工程師的技術(shù)投稿。首次或立即你可能發(fā)現(xiàn)防抖事件在等待觸發(fā)事件執(zhí)行,直到事件都結(jié)束后它才執(zhí)行。 [譯]通過實例講解Debouncing和Throtting(防抖與節(jié)流) lodash源碼中推薦的文章,為了學習(英語),翻譯了一下~ 原文鏈接 作者:DAVID CORBACHO 本文來自一位...
閱讀 3259·2021-09-22 15:58
閱讀 1724·2019-08-30 14:17
閱讀 1729·2019-08-28 18:05
閱讀 1514·2019-08-26 13:33
閱讀 692·2019-08-26 12:20
閱讀 616·2019-08-26 12:18
閱讀 3198·2019-08-26 11:59
閱讀 1412·2019-08-26 10:36