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>

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

setTimeout里面的this其实就指向btn。因为箭头函数没有thisthis指向匿名函数,而匿名函数又相当于写在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打印出来的事件参数eundefined,显然不合理,现在来解决这个问题。

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. 不能影响原函数的参数
相关推荐
视频砖家8 分钟前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能
lyj16899733 分钟前
vue-i18n+vscode+vue 多语言使用
前端·vue.js·vscode
小白变怪兽2 小时前
一、react18+项目初始化(vite)
前端·react.js
ai小鬼头2 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
墨菲安全3 小时前
NPM组件 betsson 等窃取主机敏感信息
前端·npm·node.js·软件供应链安全·主机信息窃取·npm组件投毒
GISer_Jing3 小时前
Monorepo+Pnpm+Turborepo
前端·javascript·ecmascript
天涯学馆3 小时前
前端开发也能用 WebAssembly?这些场景超实用!
前端·javascript·面试
我在北京coding4 小时前
TypeError: Cannot read properties of undefined (reading ‘queryComponents‘)
前端·javascript·vue.js
前端开发与ui设计的老司机4 小时前
UI前端与数字孪生结合实践探索:智慧物流的货物追踪与配送优化
前端·ui