函数防抖与函数节流

 


函数防抖

  可以将“防抖”理解为控制事件发生次数,在给定的时间里,如果该事件多次触发了,我们会重新计算事件处理程序的执行时间点,只执行最后一次加入队列的事件

 

下面是要进行测试的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事件
posted @   ˙鲨鱼辣椒ゝ  阅读(38)  评论(0编辑  收藏  举报
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示