一、缓冲
缓冲,这是一个非常普遍的概念,凡是使某种事物进行减慢或减弱变化过程都可以叫缓冲。网页交互中也需要缓冲,也就是 防抖(debounce)
和 节流(throttle)
—— 让高频事件的反应慢一点,虽然是老生常谈,但也不容忽视。
Lodash 中已早有实现。
-
debounce
:一个事件被触发后,立即设定一个小延时。如果在延时期间事件再次被触发,则销毁前一个延时并重新开始延时;否则,开始执行处理函数。简而言之,就是不断地重新设置计时器。 举例:电梯门,若干秒内无人进入,则自动关门。 -
throttle
:在事件连续触发的过程中,每隔一段时间执行一次处理函数。举例:文章自动保存,在编辑过程中每隔几分钟自动保存一次。
二、Debounce
1. debounce基本实现
-
需求
:鼠标在页面上移动,当移动停止 2 秒后,提示“鼠标移动了”。 -
实现
:根据前面的定义,在鼠标移动时设置新的计时器即可:
let timer = null;
function setTimer(){
timer && clearTimeout(timer);
timer = setTimeout(function () {
console.log("鼠标移动了");
}, 2000)
}
window.onmousemove = setTimer;
2. 封装
-
需求
:由于以上代码使用频繁,需要封装成一个便捷的工具函数,参数为事件处理函数和等待时长。 -
实现
:实参不一定会严格按照形参来传,注意防御
/**
* debounce函数 V1.0
* @param {function} fn 事件处理函数,默认为一个空函数
* @param {number} delay 延时时间,单位为毫秒
* @return {function} resetTimer 返回一个可以重新设定计时器的函数
*/
function debounce(fn=function(){}, delay=0){
let timer = null;
function resetTimer() {
if(timer) {clearTimeout(timer)}
// 要保证参数delay是自然数
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
timer = setTimeout(fn, delayTime);
}
return resetTimer;
}
function onMove() { console.log("鼠标移动了") }
window.onmousemove = debounce(onMove, 2000);
3. 执行环境:this
在事件处理函数中经常需要访问this对象。 上面的debounce函数被用在了window.onmousemove
上,所以onMove里的this肯定指的是window。现在我们把debounce用在别的对象上,看看this的指向会不会变化:
// ...debounce函数 V1.0
//假设有个ID为menu的元素
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 结果:window对象
出问题了,onBtnHover里的 this 并不是oBtn,问题在debounce函数的setTimeout里:
setTimeout(fn, delayTime); //相当于
function callback(){
fn();
}
setTimeout(callback, delayTime);
这里的 fn 并没有与任何对象绑定,所以它属于window对象,因此this指向window。
4. 解决this指向问题
ES5:
一个函数在被调用时,会自动取得两个特殊变量:this和arguments。在全局环境调用函数时,this代表window对象;而当函数被作为其他某个对象的方法而调用时,this就代表那个对象。
简而言之:
ES5的函数中,this是执行时函数所在的那个对象。
ES6:
this是函数定义所在的那个对象。
在下面的代码中,debounce为oBtn.onmousemove绑定了事件处理函数resetTimer。根据ES5中this的性质,该函数运行时,它的执行环境必然是调用者:
function debounce(fn=function(){}, delay=0){
function resetTimer() {
console.log("resetTimer执行环境:", this);
}
return resetTimer;
}
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 结果:执行环境是oBtn
因此,应该在resetTimer函数里将 this 保存下来,然后在setTimeout中使用call、apply函数修改this指向。debounce函数可以这样修改:
/**
* debounce函数 V2.0
* @param {function} fn 事件处理函数,默认为一个空函数
* @param {number} delay 延时时间,单位为毫秒
* @return {function} resetTimer 返回一个可以重新设定计时器的函数
*/
function debounce(fn=function(){}, delay=0){
let timer = null;
function resetTimer() {
const context = this; /**** 修改:保存执行环境 ****/
if(timer) {clearTimeout(timer)}
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
timer = setTimeout(
function(){fn.apply(context)}, /**** 修改:应用执行环境 ****/
delayTime
);
}
return resetTimer;
}
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log("鼠标移动了,执行环境是:", this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 结果:执行环境是 oBtn
5. 参数传递
上面的代码保证了this指向正确,但是一旦试图为onBtnHover传参,就会影响到this指向。
-
需求
:onBtnHover函数打印的是为其传递的字符串 -
实现
:用一个匿名函数调用onBtnHover并传参
// ...debounce函数 V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){onBtnHover("鼠标移动了,执行环境是:")},
1000
);
// 结果:执行环境是 window
-
原因
: 为了传参,我们用一个匿名函数来调用onBtnHover,因此apply(context)
只是修改了这个匿名函数的this,而onBtnHover的this仍然未被修改。 -
证明
:在这个匿名函数内打印一下this
// ...debounce函数 V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){
console.log("匿名函数执行环境:", this); /*** 修改 ***/
onBtnHover("鼠标移动了,执行环境是:");
},
1000
);
// 匿名函数执行环境:oBtn
// 鼠标移动了,执行环境是:window
-
解决方法1
: 在匿名函数里使用call、apply修改onBtnHover的this,并传参
// ...debounce函数 V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){
console.log("匿名函数执行环境:", this);
onBtnHover.call(this, "鼠标移动了,执行环境是:"); /*** 修改 ***/
},
1000
);
// 匿名函数执行环境:oBtn
// 鼠标移动了,执行环境是:oBtn
注意:以上代码中如果使用apply,则参数必须以数组的形式传递:onBtnHover.apply(this, ["鼠标移动了,执行环境是:"])
call()
方法与apply()
方法的作用相同,它们的区别仅在于接收参数的方式不同。前者需要将参数列举出来,后者则需要将参数放到数组里。
-
解决方法2
:直接将onBtnHover定义为oBtn的一个方法
// ...debounce函数 V2.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text) {console.log(text, this)}; /*** 修改 ***/
oBtn.onmousemove = debounce(
function(){
console.log("匿名函数执行环境:", this);
oBtn.onBtnHover("鼠标移动了,执行环境是:"); /*** 修改 ***/
},
1000
);
// 匿名函数执行环境:oBtn指向的元素
// 鼠标移动了,执行环境是:oBtn指向的元素
6. Debounce:总结
Debounce是在绑定事件处理函数时使用的,它会为事件绑定一个不断重新设定计时器的函数。使用上面的debounce函数 V2.0 时需要向其传递真正的事件处理函数以及延时时间。 如果需要给事件处理函数传参,则要用上述的两种方式来控制this指向。
三、Throttle
节流与防抖的差别仅仅是控制方式不同,所以我们可以借鉴debounce的实现思路。
1. throttle基本实现
-
需求
:在鼠标移动的过程中,每隔1秒打印一次“鼠标在移动” -
实现
:鼠标刚移动就会设定一个计时器,同时设定一个标记,表明在计时期间若鼠标再次移动,则什么都不做,计时结束后执行处理函数,同时清除标记。这样一来,当再次移动鼠标时,新的计时器和标记就会被设定……
/**
* throttle函数 V1.0
* @param {function} fn 事件处理函数,默认为一个空函数
* @param {number} delay 延时时间,单位为毫秒
* @return {function} setNewTimer 返回一个可以按时间间隔设定计时器的函数
*/
function throttle(fn = function(){}, delay = 0){
let isTiming = false; // 是否正在计时
let timer = null;
function setNewTimer(){
if(isTiming) {return};
//开始设置计时器和标记
isTiming = true; // 设定标记,阻止下次触发
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
let context = this;
if(timer){clearTimeout(timer)}; //清除已有计时器
timer = setTimeout(function(){
fn.apply(context);
isTiming = false;
}, delayTime);
}
return setNewTimer;
}
function onMove(){console.log( "鼠标在移动" )};
window.onmousemove = throttle(onMove, 1000);
2. 传参与this指向
-
需求
:鼠标在指定的元素上移动时每隔1秒打印一次“鼠标在移动”,并且保证this指向该元素。 -
实现
:使用前面提到的两种方式即可 -
方案1
:在匿名函数里使用call、apply修改onBtnHover的this,并传参
// throttle函数 V1.0
const oBtn = document.getElementById("menu");
function onBtnHover(text){console.log( text, this )};
oBtn.onmousemove = throttle(
function(){
console.log("匿名函数执行环境:", this);
onBtnHover.call(this, "鼠标在移动,执行环境:")
}, 1000);
-
方案2
:直接将onBtnHover定义为oBtn的一个方法
// throttle函数 V1.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text){console.log( text, this )};
oBtn.onmousemove = throttle(
function(){
console.log("匿名函数执行环境:", this);
oBtn.onBtnHover("鼠标在移动,执行环境:")
}, 1000);
四、总结
至此,debounce和throttle均已实现完毕,我们还可以向它们添加更多参数来扩展其功能,比如用另外一个参数表示是否需要立即执行,等等。