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