函数防抖与函数节流
函数防抖
可以将“防抖”理解为控制事件发生次数,在给定的时间里,如果该事件多次触发了,我们会重新计算事件处理程序的执行时间点,只执行最后一次加入队列的事件。
下面是要进行测试的HTML文档和CSS样式:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Sample Page</title> <link type="text/css" rel="stylesheet" href="test.css"> <script src="test.js"></script> </head> <body> <table> <tr> <td><div id="myDiv"></div></td> <td><div id="myDiv2"></div></td> </tr> </table> </body> </html>
#myDiv{ height: 3000px; width: 200px; background-color: blue; } #myDiv2{ height: 3000px; width: 200px; background-color: red; }
接下来会为两个<div>元素分别绑定了onmousemove事件,只要鼠标在该元素上移动了,就会触发该事件。而事件处理程序会导致元素的计数器属性加1并显示在元素内容里。
示例1:
window.onload = test; //注意:这里不能写成window.onload = test(); 因为这样是将函数test的执行结果作为onload事件监听器了 function test(){
//创建一个定时器,使用“闭包”是为了让每个元素有属于自己的num局部变量来记录,互不影响;另外,执行完counter函数后num变量就存储在内部嵌套函数的作用域链上 function counter(){ let num = 1;
//这里可以理解为返回一个对象,对象有一个number属性,值为函数 return { number:function (){ return num++; } } } let d1 = document.getElementById("myDiv"); d1.n = counter()//为元素d1添加了n属性,该属性是counter函数返回的对象 let d2 = document.getElementById("myDiv2"); d2.n = counter();
//实际的事件处理程序 function count(){ console.log(this); //输出事件是在哪个元素上发生的 this.innerHTML = this.n.number(); //调用该元素n属性中的number()函数,将执行结果返回给元素的innerHTML,作为内容显示出来 }
/*防抖函数
fn:要执行的实际处理程序
wait:经过wait毫秒后将事件加入到队列(要等线程空闲后才执行,并不是经过wait毫秒后立即执行)
context:执行上下文
*/ function debounce(fn,wait,context){ clearTimeout(fn.tId); //若函数创建了tId属性,则证明队列中已有setTimeout实例,清除它 fn.tId = setTimeout(function (){ //清除后重新等待wait毫秒后将事件加入队列 fn.call(context); },wait) }
d1.addEventListener("mousemove",function (){ debounce(count,500,this); }); d2.addEventListener("mousemove",function (){ debounce(count,500,this); })
//不能这样设置事件监听器,因为debounce(count,500,d1)相当于将该函数的结果作为事件监听器了 // d1.addEventListener("mousemove",debounce(count,500,d1)); // d2.addEventListener("mousemove",debounce(count,500,d2));
示例1中,第一次触发onmousemove事件时,队列中还没有setTimeout的实例,所以下面语句会先为函数fn添加属性tId。后续再触发相同事件时,此时已有tId属性,会调用clearTimeout()函数清除队列中的setTimeout的实例(也即事件处理程序)。
fn.tId = setTimeout(function (){ fn.call(context); },wait)
在页面中可以发现,只有当我们鼠标停止的时候,才会在控制台输出并在对应元素上面递增计时器。另外,我们在蓝色<div>元素(d1)移动时,瞬间将鼠标移动到红色<div>元素(d2),你会发现鼠标最后停在哪,哪个元素的计时器就会递增,而前面移动过的元素并没有反应。这个问题主要出在上面讨论的“为函数fn添加属性tId”上,因为这个tId是整个fn函数的属性,也即onmousemove事件所绑定的事件处理程序的属性,当多个业务一起触发相同事件时,它们是共享这个tId的,因此一个业务(比如:d2元素)会清除队列中另一个业务(比如:d1元素)的事件处理程序。
下面为了解决这个问题,使用“闭包”来创建防抖函数,让每个业务能有自己独立的tId。
示例2:
function debounce(fn,wait,context){ console.log("start"); let timeoutId; return function (){ if(timeoutId){ clearTimeout(timeoutId); } timeoutId = setTimeout(function (){ fn.call(context); },wait); }; } d1.addEventListener("mousemove",debounce(count,500,d1)); //这里与示例1不同,是以debounce(count,500,d1)的执行结果作为事件监听器的,因此会将debounce内部返回的嵌套函数绑定到事件监听器上 d2.addEventListener("mousemove",debounce(count,500,d2));
//注意:不能像这样用匿名函数来包裹debounce()作为事件监听器。因为每一次事件触发就会执行一次匿名函数,这样就会重复调用debounce(),不仅没有用到返回的嵌套函数,还重复控制台输出“start” // d1.addEventListener("mousemove",function (){debounce(count,500,d1);}); // d2.addEventListener("mousemove",function (){debounce(count,500,d2);});
代码解读:
在页面加载后就会运行.js文件,执行到 d1.addEventListener("mousemove",debounce(count,500,d1)); 语句时,会先调用一次debounce(count,500,d1)函数,此时控制台输出“start”,并将内部嵌套函数作为结果返回给事件监听器(相当于下面代码),注意,此时还没有开始触发事件。也即是说,这个内部嵌套函数就绑定到了事件监听器(onmousemove事件属性)上,事件触发时就直接执行这个函数,而不是再去调用debounce(count,500,d1)。这也是为什么控制台输出“start”只会发生一次,而不是我们误以为的,每次触发事件都会输出一次。虽然debounce函数返回了且不再调用,但变量timeoutId却一直在内部嵌套函数的作用域链上,因此每个业务在添加自己的事件监听器时(语句: d1.addEventListener("mousemove",debounce(count,500,d1));),都会为自己注册一个timeoutId。
d1.onmousemove = function(){
if(timeoutId){
clearTimeout(timeoutId);
}
timeoutId = setTimeout(function(){
fn.call(context);
),wait);
};
这样,即使在两个<div>元素之间来回移动,各自的计时器都会递增,因为他们的触发的onmousemove事件互不干扰。
应用场景
- 搜索框输入
- 窗口调整
函数节流
前面的函数防抖问题中,只要我们一直移动鼠标,debounce()就会一直刷新事件的执行时间(其实是加入队列时间),只有我们停下来才会执行一次事件。有什么方法可以让事件在超出给定时间时就会执行,而不是一直刷新执行时间?这要用到函数节流。函数节流可以理解成控制事件发生频率,但不会像函数防抖那样一直刷新执行时间。应用到前面例子就是:无论我们鼠标移动得多快,事件处理程序只按它规定的频率发生。
示例3:
function throttle(fn,wait,context){ let previous = Date.now(); console.log("start"); return function (){ let now = Date.now(); if (now-previous > wait){ fn.call(context); previous = now; } } }
d1.addEventListener("mousemove",throttle(count,2000,d1));
d2.addEventListener("mousemove",throttle(count,2000,d2));
示例3使用了时间戳来进行超时判断,同理,添加事件监听器时会进行第一次throttle()函数调用,为每个业务注册一个自己的previous变量(它的值是添加监听器时的系统事件),然后将内部嵌套函数绑定给事件监听器,后续触发事件时的事件处理程序就是这个内部嵌套函数。当第一次触发事件时,会先获取当前时间(事件触发时的时间),计算出当前事件与上一次事件(初始是添加监听器时的时间)之间经过了多长时间,若超过了规定的时间(wait)则将事件处理程序加入队列,同时将previous修改为当前事件发生时间;否则证明事件触发过于频繁,不进行处理(也就没有进入代码中的if结构)。
页面中,无论我们鼠标移动得多快,它只会按规定的wait时间将事件加入队列,然后在线程空闲时执行(可以粗略地看成,一旦发生了事件,无论事件触发多快,它都是每经过wait毫秒执行一次)。
示例4:
function throttle(fn,wait,context){ let timeoutId;
console.log("start"); return function (){ if(!timeoutId){ timeoutId = setTimeout(function (){ //当回调函数执行的时候,置timeoutId=null,表示可以执行下一次事件了, // 而在前一次回调函数执行之前,由于timeoutId有值,期间所有该事件都会被忽略(进不去if语句结构) timeoutId = null; fn.call(context); },wait); } } }
示例4则是另一个版本,使用了定时器。每次事件触发时都要检查队列中是否还有事件处理程序(有的话,timeoutId就不为null),就证明事件触发太频繁,忽略这次事件处理。当轮到队列中的事件处理程序执行时,执行过程中会将timeoutId置为null,表示允许下一次事件加入队列。
应用场景
- 高频点击提交
- scroll事件
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)