JS手写防抖和节流

前言

在Web交互场景中,用户频繁触发事件(如表单提交、按钮点击)时,前端往往会产生大量重复请求。这种现象如同在服务器端敲响密集的警报铃------不仅导致带宽浪费,更可能引发服务端过载甚至雪崩效应。面对这类问题,前端工程师需要掌握两个核心技术策略:​防抖(Debounce)​ 与节流(Throttle)​。

在这个场景下,如果用户在短时间内狂点提交,就会向后端发送很多无意义的请求,会加重后端的负担。所以我们要用某种手段去限制用户在短时间内点过多请求,避免给服务器造成太大压力。

要怎么解决这个问题呢?

1.防抖 :(思想:最后一次有效) 点了一次之后开始计时,计时结束再去执行提交。如果我点了一下,但是倒计时还没结束,我又点了一下,则上一次的点击行为作废,从这一次的点击行为开始算,重新倒计时,倒计时内没有再点提交,才去执行相应代码。即在规定的时间到来之前没有二次触发行为了才去执行。(依赖定时器)

2.节流: 在规定时间内只执行一次。比如哪怕一秒钟点了10次我也最多触发一次

3.按钮禁用: 点击一次之后让按钮变成不可点击状态,等执行完后再变回可点击状态。这个方案比较简单,但是会频繁的操作dom结构

一、防抖

在规定的时间内如果没有二次触发行为,则执行,否则放弃上一次的事件行为,直接从当前行为开始重新计时。

怎么实现?

1.实现防抖效果

写一个防抖的工具函数debounce,把点击提交要执行的函数放进debounce函数里,第二个参数是倒计时。来看以下代码,为了符合 addEventListener语法,debounce函数要返回一个函数体。(具名函数debounce内部定义了一个匿名函数,内部的匿名函数被拿到函数外部去调用了,内部函数要用到外部函数的某个变量时,就会形成闭包。)

xml 复制代码
<body>
    <button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 

        function handle (){
            console.log('向后端发送请求');
            
        }
        function debounce(fn,wait){  //防抖工具函数 
            return function(){
               setTimeout(()=>{
                fn()
               },wait);

            }
        }
    </script>
</body>

但写成这样还不够,匿名函数每触发一次,就会带来一个定时器,还是会各自计时,没有实现防抖。点第二次要把前一次的定时器销毁,怎么销毁定时器呢?

有没有办法,可以实现点第二次的时候可以拿到上一次的定时器,然后把上一次定时器销毁?

我们在debounce函数里面定义一个let timer = null ,并在匿名函数里面将setTimeout()的值赋给timer。(debounce执行完后,let timer不会销毁,而是留在闭包里 ) 从始至终debounce只执行了一次,随着用户多次点击而执行的是debounce返回的函数体(那个匿名函数),第二次执行匿名函数时,那个timer其实就是上一次的定时器。如果time有值就用clearTimeout(timer)把它清除掉,也就实现了销毁上一次定时器。

xml 复制代码
<body>
    <button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 

        function handle (){
            console.log('向后端发送请求');
            
        }
        function debounce(fn,wait){    //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
             let timer = null
            return function(){   
                if(timer)   clearTimeout(timer) //清除上一次的定时器
                timer = setTimeout(()=>{
                fn()
               },wait);

            }
        }
    </script>
</body>

这样一来,就成功实现了防抖。但写成这样还是不行,还有很多东西没有考虑到。

2.修改原函数的this指向

先问一个问题,以下代码hadle里面的this指向谁?

xml 复制代码
<button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", handle);

        function handle (){
            console.log('向后端发送请求');
            console.log(this);
            
        }

    </script>

handle不是独立调用的,是btn.addEventListener帮它调用的,所以this不可能指向window。

看一下浏览器的打印就明白了,handle是被btn调用的,this指向btn(btn是dom对象)(隐式绑定)

而代码如果写成以下这样,handle就是独立调用,handle里面的this就指向了window了,显然不合理,写防抖不能修改原函数的this指向。

xml 复制代码
 <button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 想到子父组件传值

        function handle (){
            console.log('向后端发送请求');
            console.log(this);
            
            
        }
        function debounce(fn,wait){    //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
             let timer = null
            return function(){   
                if(timer)   clearTimeout(timer) //清除上一次的定时器
                timer = setTimeout(()=>{
                fn()
               },wait);

            }
        }
    </script>

现在我们要来解决这个问题,利用call把hande里面的this重新指向btn。加上fn.call(this)这行代码就可以了。

setTimeout里面的this其实就指向btn。因为箭头函数没有this,this指向匿名函数,而匿名函数又相当于写在addEventListener里面,,所以匿名函数的this就是指向btn。

scss 复制代码
 function debounce(fn,wait){    
             let timer = null
            return function(){   
                if(timer)   clearTimeout(timer) 
                timer = setTimeout(()=>{
                fn.call(this) //把handle里面的this重新指向btn
               },wait);

            }
        }

但是写成这样还不够,还没有考虑到handle事件的参数。

3.不能影响原函数的参数

什么是事件参数?

当函数被绑定在一个事件上执行时,就一定会具有一个形参,用来描述当前的事件详情。

xml 复制代码
<button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", handle);

        function handle (e){
            console.log('向后端发送请求');
            console.log(e);
            
        }

    </script>

以下是我们之前写的防抖代码,并没有考虑到handle的参数问题

xml 复制代码
<button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 想到子父组件传值

        function handle (e){
            console.log('向后端发送请求');
            console.log(e);
            
            
        }
        function debounce(fn,wait){    //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
             let timer = null
            return function(){   
                if(timer)   clearTimeout(timer) //清除上一次的定时器
                timer = setTimeout(()=>{
                fn.call(this)
               },wait);

            }
        }
    </script>

这样handle打印出来的事件参数e是undefined,显然不合理,现在来解决这个问题。

debounce函数return出来的那个匿名函数是可以接受到事件参数e的,再通过fn.call(this,e)把e传给fn就可以了。

scss 复制代码
function debounce(fn,wait){    
             let timer = null
            return function(e){   
                if(timer)   clearTimeout(timer) 
                timer = setTimeout(()=>{
                fn.call(this,e)
               },wait);

            }
        }

这样handle就接受到了事件参数了。

但还是要再优化一下,万一handle还能接受很多参数呢

scss 复制代码
function debounce(fn,wait){    //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
             let timer = null
            return function(...args){   
                if(timer)   clearTimeout(timer) //清除上一次的定时器
                timer = setTimeout(()=>{
                fn.call(this,...args)
               },wait);

            }
        }

至此,防抖大功告成!以下是完整的手写防抖代码:

xml 复制代码
<button id="btn">提交</button>

    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件

        function handle (e){
            console.log('向后端发送请求');
            console.log(e);
            console.log(this);
            
        }
        function debounce(fn,wait){    //防抖工具函数
             let timer = null
            return function(...args){   
                if(timer)   clearTimeout(timer) //清除上一次的定时器
                timer = setTimeout(()=>{
                fn.call(this,...args) //把handle里面的this重新指向btn,并且接受参数
               },wait);

            }
        }
    </script>

二、节流

在规定时间内只执行一次。

节流和防抖很相似,我们定义一个throttle函数。函数内部定义一个let preTime = null记录上次点击的时间戳(throttle执行完后,let timer不会销毁,而是留在闭包里)

每次点击用Date.now()拿到当前时间戳,用现在时间戳nowTime减去上一次点击的时间戳preTime得到时间间隔,如果时间间隔大于规定时间就执行,否则就不执行。

以下是手写节流的代码

xml 复制代码
 <button id="btn" >提交</button>


    <script>
        let btn = document.getElementById("btn");
        btn.addEventListener("click",throttle(handle,1000));
        function handle (e){
            console.log('向后端发送请求');
            console.log(this);
        }
        function throttle(fn,wait) {  // 节流函数
            let preTime = null;
          return function(...args){
            nowTime = Date.now()
            if(nowTime - preTime > wait){
              fn.call(this,...args)
              preTime = nowTime
            }
          }
        }
    </script>

这样就成功实现了节流:

三、总结

不管是手写防抖还是节流,记住以下3点就可以了。

  1. 要有防抖(节流)效果
  2. 不能修改原函数的this指向
  3. 不能影响原函数的参数
相关推荐
无限聚核几秒前
抖音 certificate.prepare 之后怎么验券?和美团 msconsume 差在哪,无限聚核
前端·微信小程序
2603_9658966210 分钟前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人25 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly1 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝1 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦1 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95272 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端