节流
节流很像我们玩游戏时的技能 cd,释放一次之后,在冷却期间不可以再次释放,那么我们很容易得出以下代码,本质上都是利用时间做判断
// 时间戳
function throttle(fun, wait) {
let previous = 0;
return function() {
const now = +new Date();
if (now - previous > wait) {
fun.apply(this, arguments);
previous = now;
}
}
}
// 定时器
function throttle(fun, wait) {
let timer;
return function() {
if (!timer) {
timer = setTimeout(() => {
fun.apply(this, arguments);
clearTimeout(timer);
timer = null;
}, wait);
}
}
}
仔细观察上述两种写法,时间戳写法的执行时机是在 wait 的开头执行(立即执行后进入冷却),而定时器写法则是在 wait 的结尾执行,那么我们可不可以同时具备这两种功能呢,而且在真实场景中,也可能会遇到此类需求
代码如下,初步具备首次执行和尾调用的功能
function throttle(func, wait) {
let previous = 0;
let timer;
return function() {
let now = +new Date();
let remain = wait - (now - previous);
if (remain <= 0) {
// 第一次会立即执行,后续执行的条件是贤者时间(必须经过了等价于 wait 的时间)
previous = now;
func.apply(this, arguments);
} else if (!timer){
// 假如第一次执行后,在极短时间内再次执行了一次,那么我们设置一个尾调用
// 比如 wait = 1000,距离第一次执行经过了 200ms,那么 remain 就是 800ms,即尾调用
// 再次执行的话,也不会重新设置尾调用(到 wait 结束之前,都是冷却期)
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = +new Date();
fun.apply(this, arguments);
}, remain);
}
};
}
我们可能认为没什么问题了,但是其实还有一个很隐蔽的坑,按照理想情况来说,我们在第二次贤者时间时,上次的尾调用已经结束并且完成了自我清理,众所周知,js 的定时器是不准确的,定时器回调的执行时机有可能晚于我们指定的时间
这是因为定时器的回调被会放在任务队列中,当时间到后,任务队列会通知主执行栈,若主执行栈的同步任务耗时过久,就可能会影响定时器的准确性,所以,我们对以上代码进行优化
function throttle(func, wait) {
let previous = 0;
let timer;
return function() {
let now = +new Date();
let remain = wait - (now - previous);
if (remain <= 0) {
if (timer) {
// 改动如下
clearTimeout(timer);
timer = null;
}
previous = now;
func.apply(this, arguments);
} else if (!timer){
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = +new Date();
fun.apply(this, arguments);
}, remain);
}
};
}
在贤者时间过后,我们发起调用时,清除一下上次的尾调用,防止因为事件循环带来的副作用
最后我们把初次调用和尾调用封装成选项,并提供取消方法,终极代码如下
leading:false 表示禁用初次调用
trailing:false 表示禁用尾调用
function throttle(func, wait, options = {}) {
let previous = 0;
let timer;
let throttled = function() {
const now = +new Date();
if (!previous && !options.leading) {
previous = now;
}
const remain = wait - (now - previous);
if (remain <= 0) {
// 第一次会立即执行,后续执行的条件是贤者时间(必须经过了等价于 wait 的时间)
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = now;
func.apply(this, arguments);
} else if (!timer && options.trailing){
// 假如第一次执行后,在极短时间内再次执行了一次,那么我们设置一个尾调用
// 比如 wait = 1000,距离第一次执行经过了 200ms,那么 remain 就是 800ms,即尾调用
// 再次执行的话,也不会重新设置尾调用(到 wait 结束之前,都是冷却期)
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = options.leading ? +new Date() : 0;
fun.apply(this, arguments);
}, remain);
}
};
throttled.cancel = function() {
clearTimeout(timer);
previous = 0;
timer = null;
};
return throttled;
}
注意这两个选项不能同时为 false,由于 trailing 为 false,不会有尾调用,所以在等待了大于 wait 的时间后,即 remain 为负数的情况下,这时函数会立即执行,违反了 leading 为 false 的配置
防抖
防抖就像送外卖一样,骑手不可能接一单就去送一单,以 10 分钟为例,如果该时间内再加一单,那么可以再等 10 分钟,直至没有新的单子后,10 分钟的末尾执行回调,可以见得,防抖默认就是 trailing 为 true 的情况
我们添加立即执行选项,直接贴出终极代码
function debounce(func, wait, options = {}) {
let timer;
return function() {
if (options.immediate) {
// 当下无定时器的情况下立即执行,否则等价于普通防抖(尾调用)
const callNow = !timer;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait);
if (callNow) {
func.apply(this, arguments)
}
} else {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments)
}, wait);
}
};
}
多多重复,百炼成钢