JavaScript 防抖与节流
作者:Seiya
时间:2019年08月22日
防抖
防抖的原理:当持续触发事件时,debounce 会合并事件且不会去触发事件,当一定时间内没有触发再这个事件时,才真正去触发事件。
我们开始尝试写第一段代码:
function debounce(func, wait) {
var timeout;
return function() {
clearTimeout(timeout)
timeout = setTimeout(func, wait)
}
}
this
以上代码存在一个问题,在开始执行回调函数时,此时的 this 会指向 window 对象。所以我们进行一些修改,得到第二版代码:
function debounce(func, wait) {
var timeout;
return function() {
var _this = this;
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(_this)
}, wait)
}
}
event 对象
接下来看另外一个问题:JavaScript 在事件处理函数中会提供事件对象 event。比如,我们有这样一个函数:
function getUserAction(e) {
console.log(e);
container.innerHTML = count++;
};
如果我们不使用 debounce 函数,这里可以正常打印出 event 对象,但是在我们实现的 debounce 函数中,只会打印出 undefined。所以我们对代码进行修改,得到第三版代码:
function debounce(func, wait) {
var timeout;
return function() {
var _this = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(_this, args)
}, wait)
}
}
立即执行
这时候的防抖函数,功能基本已经完善了,接下来考虑一个新的需求:不用等到事件停止触发后才执行,而是立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait) {
var timeout;
return function() {
var _this = this;
var args = arguments;
if(timeout) clearTimeout(timeout)
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait)
if(callNow) func.apply(_this, args)
}
}
结合“非立即执行”和“立即执行”
function debounce(func, wait, immediate) {
var timeout;
return function() {
var _this = this;
var args = arguments;
if(timeout) clearTimeout(timeout)
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null
}, wait)
if(callNow) func.apply(_this, args)
}
else {
timeout = setTimeout(function() {
func.apply(_this, args)
}, wait)
}
}
}
返回值
我们需要执行的函数可能是有返回值的,所以我们需要返回函数的执行结果。但是,当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。
function debounce(func, wait, immediate) {
var timeout, result;
return function() {
var _this = this;
var args = arguments;
if(timeout) clearTimeout(timeout)
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null
}, wait)
if(callNow) result = func.apply(_this, args)
}
else {
timeout = setTimeout(function() {
func.apply(_this, args)
}, wait)
}
return result
}
}
取消
最后在思考一个小需求:手动取消 debounce 函数。最后一版代码如下:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var _this = this;
var args = arguments;
if(timeout) clearTimeout(timeout)
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null
}, wait)
if(callNow) result = func.apply(_this, args)
}
else {
timeout = setTimeout(function() {
func.apply(_this, args)
}, wait)
}
return result
}
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
}
return debounced;
}
我们可以这样使用:
var setUseAction = debounce(getUserAction, 3000, true);
container.onmousemove = setUseAction;
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
具体效果如下图所示:
节流
节流的原理:当持续触发事件时,保证隔间时间触发一次事件。 持续触发事件时,throttle 会合并一定时间内的事件,并在该时间结束时真正去触发一次事件。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
使用时间戳
当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
具体演示如下:
使用定时器
function throttle(func, wait) {
var timeout;
var context, args;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args);
}, wait)
}
}
}
tips
第一种节流事件会立刻执行,第二种事件会在 n 秒后第一次执行。
第一种节流事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件。
结合两种节流
鼠标移入能立刻执行,停止触发的时候还能再执行一次。代码如下:
function throttle(func, wait) {
var timeout, context, args;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
underscore 源码
防抖函数
/**
* underscore 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;
// 如果当前间隔时间少于设定时间且大于0就重新设置定时器(避免设置更多的定时器,提升性能)
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
// 如果定时器不存在就创建一个
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
}
节流函数
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 表示禁用第一次执行,传入{leading: false}。
* 表示禁用停止触发的回调,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
// 如果设置了 leading,就将 previous 设为 0,用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
}
return function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
}