我们在处理事件的时候,有些事件由于触发太频繁,而每次事件都处理的话,会消耗太多资源,导致浏览器崩溃。最常见的是我们在移动端实现无限加载的时候,移动端本来滚动就不是很灵敏,如果每次滚动都处理的话,界面就直接卡死了。
因此,我们通常会选择,不立即处理事件,而是在触发一定次数或一定时间之后进行处理。这时候我们有两个选择: debounce
(防抖动)和 throttle
(节流阀)。
之前看过很多文章都还是没有太弄明白两者之间的区别,最后通过看源码大致了解了两者之间的区别以及简单的实现思路。
首先,我们通过实践来最简单的看看二者的区别:
可以看到,throttle
会在第一次事件触发的时候就执行,然后每隔wait
(我这里设置的2000ms)执行一次,而debounce
只会在事件结束之后执行一次。
有了一个大概的印象之后,我们看一看lodash
的源码对debounce
和throttle
的区别。
这里讨论默认情况
function throttle(func, wait, options) {
let leading = true,
trailing = true;
if (typeof func !== 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (typeof options === 'object') {
leading = 'leading' in options
? !!options.leading
: leading;
trailing = 'trailing' in options
? !!options.trailing
: trailing;
}
return debounce(func, wait, {
leading,
maxWait: wait,
trailing,
});
}
可以看到,throttle
最后返回的还是debounce
函数,只是指定了options
选项。那么接下来我们就集中分析debounce
。
function debounce(fn, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
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;
}
为了记录每次执行的相关信息,debounce
函数最后返回的是一个函数,形成一个闭包。
这也解释了为什么这样写不行:
window.addEventListener('resize', function(){
_.debounce(onResize, 2000);
});
这样写根本就不会调用内部的debounced
函数。
解决第一个不同
在debounced
内部呢,首先记录了当前调用的时间,然后通过shouldInvoke
这个函数判断是否应该调用传入的func
。
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));
}
可以看到,该函数返回true
的几个条件。其中需要我们引起注意的是最后一个条件,这是debounce
与throttle
的区别之一。
首先maxing
通过函数开始的几行代码判断:
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;
}
我们看到,在定义throttle
的时候, 给debounce
函数给传入了options
, 而里面包含maxWait
这个属性,因此,对于throttle
来说,maxing
为true
, 而没有传入options
的debounce
则为false
。这就是二者区别之一。在这里决定了shouldInvoke
函数返回的值,以及是否执行接下去的逻辑判断。
我们再回到debounced
这个函数:
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);
}
在第一次调用的时候,debounce
和 throttle
的 isInvoking
为true
, 且此时timerId === undefined
也成立,就返回leadingEdge(lastCallTime)
这个函数。
那么我们再来看看leadingEdge
这个函数;
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;
}
这里出现了debounce
和throttle
的第二个区别。这个函数首先是设置了一个定时器,随后返回的结果由leading
决定。在默认情况下,throttle
传入的leading
为true
,而debounce
为false
。因此,throttle
会马上执行传入的函数,而debounce
不会。
这里我们就解决了它们的第一个不同:throttle
会在第一次调用的时候就执行,而debounce
不会。
解决第二个不同
我们再回到shouldInvoke
的返回条件那里,如果在一个时间内频繁的调用, 前面三个条件都不会成立,对于debounce
来说,最后一个也不会成立。而对于throttle
来说,首先maxing
为true
, 而如果距离上一次*传入的func 函数调用 大于maxWait
最长等待时间的话,它也会返回true
。
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
那么在shouldInvoke
成立之后,throttle
会设置一个定时器,返回执行传入函数的结果。
这就是debounce
和 throttle
之间的第二个区别:throttle
会保证你每隔一段时间都会执行,而debounce
不会。
那么还有最后一个问题,那我之前设置的定时器怎么办呢?
timerId = setTimeout(timerExpired, wait);
定时器执行的是timerExpired
这个函数,而这个函数又会通过shouldInvoke
进行一次判断。
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
最后,传入的func
怎么执行的呢?下面这个函数实现:
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
饿了么的简单实现
在看饿了么的infinite scroll
这个源码的时候,看到了一个简单版本的实现:
var throttle = function (fn, delay) {
var now, lastExec, timer, context, args;
var execute = function () {
fn.apply(context, args);
lastExec = now;
};
return function () {
context = this;
args = arguments;
now = Date.now();
if (timer) {
clearTimeout(timer);
timer = null;
}
if (lastExec) {
var diff = delay - (now - lastExec);
if (diff < 0) {
execute();
} else {
timer = setTimeout(() => {
execute();
}, diff);
}
} else {
execute();
}
};
};
那么它的思路很简单:
通过
lastExec
判断是否是第一次调用,如果是,就马上执行处理函数。
随后就会监测,每次调用的时间与上次执行函数的时间差,如果小于0,就立马执行。大于0就会在事件间隔之后执行。
每次调用的时候都会清除掉上一次的定时任务,这样就会保证只有一个最近的定时任务在等待执行。
那么它与lodash
的一个最大的区别呢,就是它是关注与上次执行处理函数的时间差, 而lodash
的shouldInvoke
关注的是两次事件调用函数的时间差
。
总结
总的来说,这种实现的主要部分呢,就是时间差 和 定时器
最后,自己参照写了简单的debounce
和 throttle
: Gist求指教!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。