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. 不能影响原函数的参数
相关推荐
疯狂的沙粒5 分钟前
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
前端·uni-app·html
小妖6669 分钟前
html 滚动条滚动过快会留下边框线
前端·html
heroboyluck23 分钟前
Svelte 核心语法详解:Vue/React 开发者如何快速上手?
前端·svelte
海的诗篇_24 分钟前
前端开发面试题总结-JavaScript篇(二)
开发语言·前端·javascript·typescript
琹箐35 分钟前
ant-design4.xx实现数字输入框; 某些输入法数字需要连续输入两次才显示
前端·javascript·anti-design-vue
程序员-小李36 分钟前
VuePress完美整合Toast消息提示
前端·javascript·vue.js
Uyker1 小时前
从零开始制作小程序简单概述
前端·微信小程序·小程序
Dontla5 小时前
为什么React列表项需要key?(React key)(稳定的唯一标识key有助于React虚拟DOM优化重绘大型列表)
javascript·react.js·ecmascript
EndingCoder6 小时前
React从基础入门到高级实战:React 实战项目 - 项目三:实时聊天应用
前端·react.js·架构·前端框架
阿阳微客7 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏