前端开发工作中,我们经常在一个事件发生后执行某个操作,比如鼠标移动时打印一些东西:
1 window.addEventListener("mousemove", ()=>console.log(123)); 2 //测试发现鼠标移动了1毫米,回调函数执行了将近10次,这种做法是非常耗费资源的。
解决方法就是每进一个人都重新倒计时N秒再关门,这样只要每个人都在前一个人进去N秒之内进门,那么每进一个人,电梯都会重新计时N秒,所以它只会在最后一个人进门N秒之后再启动关门程序。
这里有三个关键点:「事件是高频率发生的」、「在前一个发生后N秒内发生下一个」、「重新倒计时」。
函数防抖(debounce)就是以上解决方案的JavaScript实现,上面代码的改写思路是:每次事件发生后都只做两件事——「清除旧的计时器」、「设置新的计时器」
//重置计时器的函数 function debounce(func, ms){ let timer = null; function reTimer(){ //重新计时 clearTimeout(timer) timer = setTimeout(func, ms) } return reTimer; } //要执行的动作 function handle(){ console.log("--- do something ---") } //绑定事件:每次鼠标移动时,就会执行debounce返回的reTimer函数 window.addEventListener("mousemove", debounce(handle, 1000))
前面说了,每次事件发生后都只做两件事——「清除旧的计时器」、「设置新的计时器」,那么为什么要在addEventListener里执行debounce函数呢?
因为reTimer函数需要操作来自父级作用域的变量timer,而debounce函数就是为了创建这样一个作用域,使得每次执行reTimer函数时timer变量都是存在的。
如果要求不使用debounce函数,我们就得把timer变量定义在addEventListener之前:
//重置倒计时 function reTimer(){ if(timer){ clearTimeout(timer) } timer = setTimeout(handle, 1000) } //事件处理 function handle(){ console.log("--- do something ---") } //事件绑定 let timer = null; //或者 window.timer = null window.addEventListener("mousemove", reTimer)
不过,相比使用debounce函数,这样做就不那么优雅了。
addEventListener的目的是操作timer变量,而timer在debounce的作用域内,addEventListener访问不到,所以用debounce返回的reTimer去访问,这就是闭包了。
防抖是让重复事件的处理函数只在最后一次发生时执行,而闭包只是一个更好的实现方案。
二、函数节流
理解了函数防抖,函数节流也就好办了,我们只需要理解场景和方案。
假如我们正在做一个输入框,要求每输入一个字符都调用一个API来查询数据,从而实现联想、自动补全等功能,然而我们的输入速度是很快的,可能还没等第一个字符的查询结果出来,第二个字符就已经敲进去了,所以我们需要让查询频率小一点,具体做法就是在输入的过程中,每隔N秒才查询一次。
这里的关键点是:「事件是高频率发生的」、「在前一个发生后N秒内发生下一个」、「一个计时结束后再重新计时」
定时器实现节流
节流函数(throttle)
/* * 节流函数生成器 * 传递事件处理函数和延迟时间 * 返回节流函数 */ function throttleGen(fn, delay) { let timer = null; function throller() { if (timer === null) { timer = setTimeout(function () { fn(); timer = null; }, delay) } } return throller; } //事件处理函数 function handle() { console.log('-- do something --'); } //绑定事件 window.addEventListener("mousemove", throttleGen(handle, 1000))
三、防抖和节流的对比
function debounce(fn, delay) { let timer = null; return function () { var _this = this; //这里改了 clearTimeout(timer); timer = setTimeout(function () { fn.apply(_this); //这里改了 }, delay); }; }