之前看过防抖节流但是忽略了一些细节的东西,比如防抖是从第一个触发事件开始还是从最后一个触发事件开始。
参考文章
函数防抖(debounce)
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
在多次事件重复触发后,延迟一定的时间在执行这次事件。如下图,在事件触发 400ms
延迟之内如果再次触发,将会重置计时器,直到 400ms
内没有事件触发之后,再真正执行该事件方法。
当然也可以在开始触发事件的时候直接去执行,之后重复触发的事件不再执行,中间的间隔时间也是 400ms
。
例如:只要按下键盘,就会触发 Ajax
请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。加入了防抖以后,当频繁的输入时,并不会发送请求,只有在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。
以下是最简单的防抖函数,注意这里没有绑定 this
的指向。
const debounce = (fn, delay) => {
let time;
return (...args) => {
if (time) {
clearTimeout(time);
}
time = setTimeout(() => {
fn(...args);
}, delay);
};
};
案例一:容器点击事件
案例二:输入框输入事件
函数节流(throttle)
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
By using
_.throttle
, we don’t allow to our function to execute more than once every X milliseconds.
在节流方面,我们可以也可以通过定时器来实现节流。通过使用定时任务,延时方法执行。在延时的时间内,方法若被触发,则直接退出方法。从而,实现函数一段时间内只执行一次。
const throttle = (fn, delay) => {
let timer;
return (...args) => {
// 如果有存在 timer 说明,还在这个区间内。
if (timer) return;
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
};
};
根据函数节流的原理,其实这里我们也可以不用 setTimeout 实现节流,可以直接通过闭包返回一个函数并且用到闭包函数外面的变量 last,两者原理是相同的。
const throttle = (fn, delay) => {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last < delay) {
return;
}
last = now;
return fn(...args);
};
};
正如参考文章中所提到的,最常规的一个案例是,用户正在向下滚动您的无限滚动页面。需要检查用户离底部有多远,如果用户接近底部,需要通过 Ajax
请求更多内容并将其附加到页面。
案例一:滚动请求加载更多
Lodash 实现
Lodash 官方给出 debounce 比较完善,包含一下六个参数。具体实现:debounce
func
(Function): 要防抖动的函数。[wait=0]
(number): 需要延迟的毫秒数。[options=]
(Object): 选项对象。[options.leading=false]
(boolean): 指定在延迟开始前调用。[options.maxWait]
(number): 设置func
允许被延迟的最大值。[options.trailing=true]
(boolean): 指定在延迟结束后调用。
const debounce = (fn, wait, options) => {
// option: {leading: boolean, trailling: boolean, maxWait: number}
// 上次调用的时间
let lastCalltime;
// 真实调用的时间
let lastInvokeTime = 0;
// 定时器ID
let timerId;
// 最大等待时间
let maxWait;
// 控制 maxWait 的标志
let maxing = false;
// 是否在头调用
let leading = false;
// 是否在尾调用
let trailing = true;
// 函数结果
let result;
// 所有参数
let allArgs;
if (options) {
leading =
"leading" in options
? !!options.leading
: leading;
trailing =
"trailing" in options
? !!options.trailing
: trailing;
maxWait = options.maxWait;
maxing = "maxWait" in options;
}
function debounced(...args) {
const time = Date.now();
const canInvoke = shouldInvoke(time);
lastCalltime = time;
allArgs = args;
if (canInvoke) {
// 起始调用 (第一次 或 一次调用完整结束后)
if (timerId === undefined) {
return leadingEdge(time);
}
}
return result;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCalltime;
const timeSinceLastInvoke =
time - lastInvokeTime;
// 1. 第一次调用
// 2. 间隔时间大于wait
// 3. 等待时间大于了maxWait
return (
timerId === undefined ||
timeSinceLastCall >= wait ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}
function leadingEdge(time) {
lastInvokeTime = time;
// 调用的开始,等待完整的wait
timerId = setTimeout(timerExpired, wait);
if (leading) {
// 处理leading
return invokeFunc(time);
}
return result;
}
function trailingEdge(time) {
// 运行trailling意味着一次调用的结束
timerId = undefined;
if (trailing) {
return invokeFunc(time);
}
return result;
}
function timerExpired() {
const time = Date.now();
// 再次检查是否可以调用
// 多次点击会改变lastCallTime, 所以shouldInvoke返回值也会改变
if (shouldInvoke(time)) {
// 处理trailling
return trailingEdge(time);
}
// 不满足条件,继续等待
// remainingWait来计算需要等待的具体时间
timerId = setTimeout(
timerExpired,
remainingWait(time)
);
}
function invokeFunc(time) {
lastInvokeTime = time;
const argsCopy = allArgs;
allArgs = undefined;
if (allArgs) {
return fn(...argsCopy);
}
return fn();
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCalltime;
const timeSinceLastInvoke =
time - lastInvokeTime;
const timeToWait = wait - timeSinceLastCall;
// maxing模式,返回 正常需要等待的时间 和 距离maxWait剩余时间 的小值
return maxing
? Math.min(
timeToWait,
maxWait - timeSinceLastInvoke
)
: timeToWait;
}
return debounced;
};
总结
debounce
search
搜索联想,用户在不断输入值时,用防抖来节约请求资源。window
触发resize
的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
throttle
- 鼠标不断点击触发,
mousedown
(单位时间内只触发一次) - 监听滚动事件,比如是否滑到底部自动加载更多,用
throttle
来判断