有时候,咱们会对一些触发频率较高的事件进行监听,若是在回调里执行高性能消耗的操做,反复触发时会使得性能消耗提升,浏览器卡顿,用户使用体验差。或者咱们须要对触发的事件延迟执行回调,此时能够借助throttle/debounce函数来实现需求。javascript
throttle函数用于限制函数触发的频率,每一个delay
时间间隔,最多只能执行函数一次。一个最多见的例子是在监听resize/scroll
事件时,为了性能考虑,须要限制回调执行的频率,此时便会使用throttle函数进行限制。css
由throttle函数的定义可知,每一个delay
时间间隔,最多只能执行函数一次,因此须要有一个变量来记录上一个执行函数的时刻,再结合延迟时间和当前触发函数的时刻来判断当前是否能够执行函数。在设定的时间间隔内,函数最多只能被执行一次。同时,第一次触发时当即执行函数。如下为throttle实现的简略代码:java
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函数一样能够减小函数触发的频率,但限制的方式有点不一样。当函数触发时,使用一个定时器延迟执行操做。当函数被再次触发时,清除已设置的定时器,从新设置定时器。若是上一次的延迟操做还未执行,则会被清除。一个最多见的业务场景是监听onchange事件,根据用户输入进行搜索,获取远程数据。为避免屡次ajax请求,使用debounce函数做为onchange的回调。jquery
由debounce的用途可知,实现延迟回调须要用到setTimeout
设置定时器,每次从新触发时须要清除原来的定时器并从新设置,简单的代码实现以下:git
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函数。github
上文中实现的代码较为简单,未考虑参数类型的判断及配置、测试等。下面介绍部分实现throttle和debounce的开源的类库。ajax
$.throttle
指向函数jq_throttle
。jq_throttle
接收四个参数 delay, no_trailing, callback, debounce_mode
。参数二no_trailing
在throttle模式中指示。除了在文档上说明的三个参数外,第四个参数debounce_mode
用于指明是不是debounce模式,真即debounce模式,不然是throttle模式。segmentfault
在jq_throttle
函数内,先声明须要使用的变量timeout_id
(定时器)和last_exec
(上一次执行操做的时间),进行了参数判断和交换,而后定义了内部函数wrapper
,做为返回的函数。浏览器
在wrapper
内,有用于更新上次执行操做的时刻并执行真正的操做的函数exec
,用于清除debounce模式中定时器的函数clear
,保存当前触发时刻和上一次执行操做时刻的时间间隔的变量elapsed
。app
若是是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中相比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 }); }