在上一遍笔记里已经实现了防抖函数,这次要实现一下节流函数。节流函数也是限制事件的频繁触发。
节流的原理:
如果持续触发事件,每隔一段时间,只执行一次。根据首次是否执行以及结束是否执行,效果有所不同,实现方式也有所不同。
有两种主流的实现方式:使用时间戳,使用定时器。
时间戳
当触发事件的时候,取出当前的时间戳,然后减去之前的时间戳(一开始设置值为 0),如果大于设置的时间周期(wait),就执行函数,然后更新时间戳为当前时间的时间戳,如果小于,就不执行。
// 第一版
function throttle(func, wait) {
let context, args, previous = 0;
return function () {
let now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now
}
};
}
还是以防抖函数里面的例子为例
当鼠标初次移入时,会立即触发执行,然后每过一秒会执行一次,如果在 4.3s 停止触发,以后不会再执行事件。
定时器
当触发事件的时候,设置一个定时器,在触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清理定时器,这样就可以设置下一个定时器。
// 第二版
function throttle(func, wait) {
let timeout;
return function () {
const context = this, args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
当鼠标初次移入的时候,事件不会立即执行,晃了 2s 后终于执行了一次,之后每隔 2s 执行一次,当数字显示为 3 时,立即移出鼠标,相当于在 6.2s 的时候停止触发,但是在第 8s 的时候执行一次事件。
合二为一
那能不能写一个有头有尾的呢?既鼠标初次移入时立即执行一次,停止触发的时候还能再执行一次。综合时间戳和定时器的优势。
// 第三版
function throttle(func, wait) {
let timeout, context, args;
let previous = 0;
const later = function () {
previous = +new Date();
timeout = null;
func.apply(context, args);
};
return function () {
const now = +new Date();
//下次触发 func 剩余的时间
const remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
// 首次触发或者触发的时候距离上一次的触发时间大于 wait 时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
// 距离上次触发的时间小于 wait 时间
// 不管触发几次还是以第一次为主
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
}
可以看到当鼠标初次移入的时候,会立即执行一次,之后每隔 2s 执行一次,当数字是 2 的时候立即移出鼠标,也就时大概在 4.2s 的时候停止触发事件,但是在 6s 的时候也会执行一次触发。
优化
能不能在首次触发结尾不触发,或者首次不触发结尾触发了?
这是需要一个 options 作为第三个参数,它有两个值
leading:false 表示禁用第一次执行。
trailing:false 表示禁用停止触发的回调。
// 第四版
function throttle(func, wait, options) {
let timeout, context, args;
let previous = 0;
const later = function () {
previous = options.leading === false ? 0 : +new Date();
timeout = null;
func.apply(context, args);
context = args = null;
};
const throttled = function () {
const now = +new Date();
if (!previous && options.leading === false) previous = now;
//下次触发 func 剩余的时间
const remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
// 首次触发或者触发的时候距离上一次的触发时间大于 wait 时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
// 距离上次触发的时间小于 wait 时间
// 不管触发几次还是以第一次为主
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
取消
在 throttle 函数内部添加以下代码
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = null;
};
注意
leading: false
和 trailing: false
不能同时设置,如果同时设置的话,当鼠标初次移入时不会触发执行,停止触发时不会设置 later,但只要过了 wait 规定的时间,再次移入的话,就会立即执行,违反了 leading: false
。
所以只有三种写法
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
参考: