对于持续触发的事件,规定一个间隔时间(n秒),每隔一段只能执行一次。
上一篇讲的函数防抖(debounce)与本篇说的函数节流(throttle)相似又不同。
函数防抖一般是指对于在事件被触发n秒后再执行的回调,如果在这n秒内又重新被触发,则重新开始计时。
二者都能防止函数过于频繁的调用。
区别在于,当事件持续被触发,如果触发时间间隔短于规定的等待时间(n秒),那么
函数节流的实现有不同的思路,可以通过时间戳实现,也可以通过定时器实现。
只要触发,就用 Date 获取现在的时间,与上一次的时间比较。
function throttle(func, wait) {
let previous = 0;
return function() {
let now = +new Date();
let context = this;
if (now - previous >= wait) {
func.apply(context, arguments);
previous = now; // 执行后更新 previous 值
}
}
}
示意
container.onmousemove = throttle(doSomething, 1000);
用定时器实现时间间隔。
注①:若要防抖,就马上取消它,重新定义计时器以重新计时。见函数防抖(debounce)
function throttle(func, wait) {
let time, context
return function(){
context = this
if(!time){
time = setTimeout(function(){
func.apply(context, arguments)
time = null
}, wait)
}
}
}
一个周期内:
结合二者,实现一次触发,两次执行(先立即执行,结尾也有执行)
function throttle (func, wait) {
let previous = 0
let context, args, time
return function(){
let now = +new Date()
context = this;
args = arguments
if(now - previous >= wait){ //当距上一次执行的间隔大于规定,可以直接执行
func.apply(context, args)
previous = now
} else { // 否则继续等待,结尾执行一次
if(time) clearTimeout(time)
time = setTimeout(
() => {
func.apply(context, args)
time = null
}
, wait)
}
}
}
已经实现了一次触发,两次执行,有头有尾的效果。
问题是,上一个周期的“尾”和下一个周期的“头”之间,失去了对时间间隔的控制。
仔细查看,发现问题出在了 previous
的设置上。
仅仅在“可直接执行”的情况下更新了 previous
值,在通过计时器注册入任务队列后执行的情况下,忽略了 previous
的更新。导致了 previous
的值不再是“上一次执行”时的时间,而是“上一次直接可执行情况下执行”的时间。
同时,引入变量 remaining
表示还需要等待的时间,来让尾部那一次的执行也符合时间间隔。
function throttle(func, wait) {
let previous = 0;
let context, args, time, remaining;
return function() {
let now = +new Date();
context = this;
args = arguments;
remaining = wait - (now - previous)// 剩余的还需要等待的时间
if (remaining <= 0) {
func.apply(context, args);
previous = now // 重置“上一次执行”的时间
} else {
if (time) {
clearTimeout(time);
}
time = setTimeout(() => {
func.apply(context, args);
time = null;
previous = +new Date() // 重置“上一次执行”的时间
}, remaining) //等待还需等待的时间
}
};
}
参考 underscore 与 mqyqingfeng ,实现是否启用第一次 / 尾部最后一次计时回调的执行。
设置 options
作为第三个参数,然后根据传的值判断到底哪种效果,约定:
参考代码:
function throttle(func, wait, options) {
let time, context, args, result;
let previous = 0;
if (!options) options = {};
let later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
time = null;
func.apply(context, args);
if (!time) context = args = null;
};
let throttled = function() {
let now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (time) {
clearTimeout(time);
time = null;
}
previous = now;
func.apply(context, args);
if (!time) context = args = null;
} else if (!time && options.trailing !== false) {
time = setTimeout(later, remaining);
}
};
return throttled;
}
在 函数防抖(debounce)中,实现了一个 cancel 方法。同理:
...
throttled.cancel = function() {
clearTimeout(time);
time = null;
previous = 0;
}
...
参考:https://github.com/mqyqingfeng/Blog/issues/26