在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
函数防抖(debounce)和节流(throttle)就是控制事件触发频率的方法,是针对响应跟不上触发频率这类问题的两种解决方案。
假设一个事件被频繁触发的场景:
鼠标滑过一个div,触发onmousemove事件,然后显示当前坐标。
let box = document.getElementById('box');
function getPosition(e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`;
}
box.onmousemove = getPosition;
可以看到事件在频繁执行,由于这只是一个简单的例子,不会有什么影响。但如果是一个复杂的回调函数,执行这么多次,势必会发生卡顿,给用户带来不好的体验。
我们用防抖和节流两种办法来控制事件触发频率。
防抖
防抖的原理:触发事件后的第n秒执行
两种实现:①非立即执行,②立即执行
- 非立即执行
持续触发事件不会执行,在停止触发事件后按最后一次触发事件的时间来计时,n秒后执行。
如果在等待执行的这n秒内又触发了事件,那时间就按这一次触发的时间重新计算,等待n秒执行。
// 简单实现
// 用 setTimeout设置定时器
function debounce(fun, wait){
let timeout;
return function(){
// 如果持续触发,那么就清除定时器,定时器的回调就不会执行。
if(timeout) clearTimeout(timeout);
timeout = setTimeout(fun,wait);
}
}
box.onmousemove = debounce(getPosition,1000);
// 但直接这样实现是存在问题的
// 在 getPosition 函数中打印 console.log(this + e)
// 调用debounce后 ---> this指向window, e为undefined
// 不调用debounce ---> this指向box, e为MouseEvent
// 所以需要解决 this 指向 和 event 对象这两个问题
解决:this 指向 和 event 对象问题
function debounce(fun, wait){
let timeout;
return function(){
let _this = this;
let args = arguments;
if(timeout) clearTimeout(timeout);
timeout = setTimeout(function(){
fun.apply(_this, args);
},wait);
}
}
box.onmousemove = debounce(getPosition,1000);
- 立即执行
立刻执行函数,然后等到停止触发 n 秒后,再重新触发执行。
// 增加一个参数来选择是否要立即执行
// 主要增加 if(immediate){}中的内容
function debounce(fun, wait, immediate){
let timeout;
return function(){
let _this = this;
let args = arguments;
if(timeout) clearTimeout(timeout);
if(immediate){
let callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
// timeout 为空时才会执行
if (callNow) fun.apply(_this, args);
}
else{
timeout = setTimeout(function(){
fun.apply(_this, args);
}, wait);
}
}
}
box.onmousemove = debounce(getPosition,1000,true);
节流
节流的原理:持续触发事件,每隔一段时间,只执行一次事件。
两种实现:①使用时间戳,②设置定时器
- 时间戳
当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳。如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(fun, wait) {
let previous = 0;
return function() {
let now = +new Date();
let _this = this;
let args = arguments;
if (now - previous > wait) {
fun.apply(_this, args);
previous = now;
}
}
}
box.onmousemove = throttle(getPosition,3000);
- 定时器
当触发事件的时候,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(func, wait) {
let timeout;
return function() {
let _this = this;
let args = arguments;
// timeout为空时执行
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(_this, args)
}, wait)
}
}
}
box.onmousemove = throttle(getPosition,3000);
参考文章:
JavaScript专题之跟着underscore学防抖 #22
JavaScript专题之跟着 underscore 学节流 #26