以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
window
对象的resize
、scroll
事件mousemove
事件mousedown
、keydown
事件keyup
事件实际上对于window的resize
事件,实际需求大多为停止改变大小n
毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。
throttle(又称节流)和debounce(又称去抖)其实都是函数调用频率的控制器,
当调用函数n
秒后,才会执行该动作,若在这n
秒内又调用该函数则将取消前一次并重新计算执行时间,举个简单的例子,我们要根据用户输入做suggest,每当用户按下键盘的时候都可以取消前一次,并且只关心最后一次输入的时间就行了。
在lodash中提供了debounce函数:
_.debounce(func, [wait=0], [options={}])
lodash在opitons
参数中定义了一些选项,主要是以下三个:
leading
,函数在每个等待时延的开始被调用,默认值为false
trailing
,函数在每个等待时延的结束被调用,默认值是true
maxwait
,最大的等待时间,因为如果debounce的函数调用时间不满足条件,可能永远都无法触发,因此增加了这个配置,保证大于一段时间后一定能执行一次函数根据leading
和trailing
的组合,可以实现不同的调用效果:
leading
-false
,trailing
-true
:默认情况,即在延时结束后才会调用函数leading
-true
,trailing
-true
:在延时开始时就调用,延时结束后也会调用leading
-true
, trailing
-false
:只在延时开始时调用deboucne还有cancel
方法,用于取消防抖动调用
下面是一些简单的用例:
// 避免窗口在变动时出现昂贵的计算开销。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
// 当点击时 `sendMail` 随后就被调用。
jQuery(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
// 确保 `batchLog` 调用1次之后,1秒内会被触发。
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
// 取消一个 trailing 的防抖动调用
jQuery(window).on('popstate', debounced.cancel);
在学习Vue的时候,官网也用到了一个里子,就是用于对用户输入的事件进行了去抖,因为用户输入后需要进行ajax
请求,如果不进行去抖会频繁的发送ajax
请求,所以通过debounce对ajax
请求的频率进行了限制
完整的demo在这里。
methods: {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
getAnswer: _.debounce(function() {
if (!reg.test(this.question)) {
this.answer = 'Questions usually end with a question mark. ;-)';
return;
}
this.answer = 'Thinking ... ';
let self = this;
axios.get('https://yesno.wtf/api')
// then中的函数如果不是箭头函数,则需要对this赋值self
.then((response) = > {
this.answer = _.capitalize(response.data.answer)
}).
catch ((error) = > {
this.answer = 'Error! Could not reach the API. ' + error
})
}, 500) // 这是我们为判定用户停止输入等待的毫秒数
},
一个简单的手写的去抖函数:
function test() {
console.log(11)
}
function debounce(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context)
}, 500)
}
window.onresize = function() {
debounce(test, window);
}
function debounce(func, wait, options) {
var nativeMax = Math.max,
toNumber,
nativeMin
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
// func 上一次执行的时间
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
// func必须是函数
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
// 对间隔时间的处理
wait = toNumber(wait) || 0;
// 对options中传入参数的处理
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;
}
// 在leading edge阶段执行函数
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// 为 trailing edge 触发函数调用设定定时器
timerId = setTimeout(timerExpired, wait);
// leading = true 执行函数
return leading ? invokeFunc(time) : result;
}
// 剩余时间
function remainingWait(time) {
// 距离上次debounced函数被调用的时间
var timeSinceLastCall = time - lastCallTime,
// 距离上次函数被执行的时间
timeSinceLastInvoke = time - lastInvokeTime,
// 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
result = wait - timeSinceLastCall;
// 两种情况
// 有maxing: 比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
// 无maxing: 在下一次trailing时执行timerExpired
return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
}
// 根据时间判断 func 能否被执行
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// 几种满足条件的情况
return (lastCallTime === undefined // 首次执行
|| (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
|| (timeSinceLastCall < 0)// 系统时间倒退
|| (maxing && timeSinceLastInvoke >= maxWait)); //超过最大等待时间
}
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 重启定时器
timerId = setTimeout(timerExpired, remainingWait(time));
}
// 在trailing edge阶段执行函数
function trailingEdge(time) {
timerId = undefined;
// 有lastArgs才执行,
// 意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
if (trailing && lastArgs) {
return invokeFunc(time);
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
lastArgs = lastThis = undefined;
return result;
}
// cancel方法
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
// flush方法--立即调用
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
//是否满足时间条件
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time; //函数被调用的时间
// 无timerId的情况有两种:
// 1.首次调用
// 2.trailingEdge执行过函数
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
throttle将一个函数的调用频率限制在一定阈值内,例如1s
内一个函数不能被调用两次。
同样,lodash提供了这个方法:
_.throttle(func, [wait=0], [options={}])
具体使用的例子:
// 避免在滚动时过分的更新定位
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
// 点击后就调用 `renewToken`,但5分钟内超过1次。
var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
jQuery(element).on('click', throttled);
// 取消一个 trailing 的节流调用。
jQuery(window).on('popstate', throttled.cancel);
throttle同样提供了leading
和trailing
参数,与debounce含义相同
其实throttle就是设置了
maxwait
的debounce
注意,debounce返回的是一个经过包装的函数,被包装的函数必须是要立刻执行的函数。例如:
function test() {
console.log(123)
}
setInterval(function () {
_.debounce(test, 1500)
}, 500)
上面的效果不会是我们想要的效果,因为每次setInterval
执行之后,都返回了一个没有执行的、经过debounce包装后的函数,所以debounce是无效的
点击事件也是同样:
btn.addEventListener('click', function () {
_.debounce(test, 1500)
})
上面的代码同样不会生效,正确的做法是:
btn.addEventListener('click', test)
setInterval(_.debounce(test, 1500), 500)