例如 scroll、input 等事件频繁触发时,可能造成性能问题,若事件处理时使用了异步方法(例如 ajax 请求),还有可能导致结果错误,例如输入框输入时自动 ajax 请求查询数据,若不进行处理,则会频繁触发 ajax 请求,且显示的查询结果可能不是当前输入内容的返回结果。
概念
- 设定一个周期,期间不执行动作
- 前缘 leading 和延迟 trailing
- 前缘:执行动作后再开始周期
- 延迟:周期结束后再执行动作
- 防抖 debounce 和节流 throttle
- 防抖:若期间动作又被触发,则重新设定周期
- 节流:周期固定
应用场景
防抖 debounce
简单来说,就是在一段时间大量频繁触发事件时,只在最后一次事件触发时执行动作。
- 比如输入框 input onchange,输入查询字符串时实时搜索相应结果,只需要查询最后输入完成的字符串。
- 又或者窗口 resize 后重新计算页面某一元素尺寸 width/height,只需要 resize 完成后进行计算。
节流 throttle
而有些情况下则需要限制频率的同时多次触发,而非只是最后一次。
例如最常见的监听 scroll 事件并动态加载某一元素(例如返回顶部按钮,或是懒加载图片等等)
lodash 实现
防抖 debounce
/**
* 部分代码有一定简化
* fn: 执行函数
* wait: 周期时间
* leading: true 表示前缘执行,false 表示延迟执行
* maxWait: 如果设置了,就表示固定 maxWait 时间后一定执行
*/
function debounce(fn, wait, leading = false, maxWait) {
let result;
let lastThis;
let lastCallTime;
let lastInvokeTime = 0;
let timerId;
if (maxWait !== undefined) maxWait = Math.max(maxWait || 0, wait);
// 开始周期
function leadingEdge(time) {
lastInvokeTime = time;
timerId = setTimeout(timerExpired, wait);
// 若设置了前缘 leading,则立即执行一次 fn
return leading ? invokeFunc(time) : result;
}
// 结束周期
function timeExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
timerId = undefined;
if (!leading && lastArgs) {
// lastArgs 表示 fn 至少 debounced 了一次
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
}
return result;
}
// 执行动作
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = fn.call(thisArg, ...args);
return result;
}
// 判断是否处于可执行阶段
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined // 第一次调用
|| timeSinceLastCall >= wait // 已经过了一个周期不需要等待了
|| timeSinceLastCall < 0 // shouldInvoke 调用已经过时了
|| (maxWait && timeSinceLastInvoke >= maxWait) // 到了固定周期执行时间了
);
}
function debounced(...args) {
const time = Date.now();
lastThis = this;
lastArgs = args;
lastCallTime = time;
if (shouldInvoke(time)) {
if (timerId === undefined) {
// 没有等待的周期
return leadingEdge(lastCallTime);
}
if (maxWait) {
// 有设置固定强制执行周期时间时,强制调用
timerId = setTimeout(timeExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
// 第一次调用
timerId = setTimeout(timeExpired, wait);
}
return result;
}
return debounced;
}
节流 throttle
function throttle(fn, wait, leading) {
// throttle 和 debounce 的差别在于固定周期执行,即 maxWait 设置为 wait
return debounce(fn, wait, leading, wait);
}