彻底搞懂防抖的实现原理(this,闭包等细节)
2024-09-15 14:14 阅读(227)

前言

在前端开发中,我们经常需要处理一些高频触发的事件,如窗口的scroll事件、按钮的click事件以及输入框的input事件等。如果不加处理,这些事件会大量消耗系统资源,并且可能影响用户的体验。这时候,就需要用到防抖(debounce)技术来减少事件处理函数的调用频率。

防抖的概念

防抖是一种常用的优化手段,它能够保证在一段指定的时间内,某个函数最多只被执行一次。如果在这段时间内,该事件再次被触发,则不会执行任何动作,而是重新开始计时。直到这段时间过去后,才会执行一次事件处理函数。

正文

想象一下这个场景:有很多人同时访问一个网站,并且有很多人都在点网站的同一个按钮发请求,这很正常对吧,如果这个时候服务器很卡,这时很多用户的页面加载不出来,当遇到这个时候有很多人都会点了又点按钮,甚至狂点对吧,那这个时候,每点一次就发送一次请求,还是同一个请求,这时后台服务器就更忙不过来,很可能就蹦掉了,所以这时我们就要设置防抖,当用户一直连续点击按钮时,不发送请求到后端,而是停止点击后发送请求,这样就能给我们后台服务器减轻压力。

不加防抖的按钮

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">提交</button>
  <script>
    let btn = document.getElementById('btn')
    function handle() {
      // 模拟请求
      console.log('提交');
    }
    btn.addEventListener('click', handle)
  </script>
</body>
</html>

效果:

可以看到每点一次就提交一次,下面我们一步一步来实现防抖效果吧。


实现防抖的过程

1. 我们想要按钮在规定的一段时间内一直点击没有用,只有点完按钮后松开不点击,并且过了我们规定的时间,才能提交对吧。


那我们是不是可以这样做:

<body>
  <button id="btn">提交</button>
  <script>
    let btn = document.getElementById('btn')
    function handle() {
      // 模拟请求
      console.log('提交成功');
    }
    btn.addEventListener('click', function(){
        let timer=null
        clearTimeout(timer);
        timer=setTimeout(function(){
        handle();
     },1000)
})
  </script>
</body>

我们把 handle函数放定时器里,当点击按钮我就触发最外面的function,点击一次我就把上一次的setTimeout清除,直到你不点了,1秒后我就提交。


效果:

但是我们看到实际并不是我们想的那个对吧


 let timer = null; // 每次点击时都会重新声明timer
 clearTimeout(timer); // 清除timer,但由于它是新声明的,实际上没有效果

问题就出现在我们无法让我们清除这一次的定时器和上次的定时器是同一个对吧,但是思路没有问题。

2. 我们知道闭包的作用是不是可以实现变量私有化,那它就可以让我们的定时器一直是闭包里面的那个定时器。如果不知道闭包可以看我的这篇文章<你不知道的JavaScript>,里面有介绍到这里用到的知识点:this、闭包、调用栈。

我们照这个思路修改代码:

<body>
  <button id="btn">提交</button>

  <script>
    let btn = document.getElementById('btn')
    function handle() {
      // 模拟请求
      console.log('提交');
    }
    
    btn.addEventListener('click', debounce(handle))

    // 防抖函数
    function debounce(fn) {
      let timer = null
      return function() {
        // 如果第二次的时间没到1s,就销毁上一次的定时器
        clearTimeout(timer)
        timer = setTimeout(function(){
          fn()
        }, 1000)
        
        // 也可以这样
        //timer = setTimeout(fn,1000)
      }
      
    }
  </script>
</body>

效果:

可以看到成功实现了防抖效果

解释: 我们写了一个debounce函数,它形参是我们的handle函数,我们这里不是handle(),不是handle()函数调用,只是把它当参数传进来,并且 let timer = null,声明了一个 timer变量,然后return出去了一个function,这是闭包的典型了,那么  btn.addEventListener('click', debounce(handle))这里,是不是我点击按钮,就相当于触发return出来的这个function,function里面还创建了定时器,定时器到时间后就执行里面的fn,也就是我们外面的handel函数,并且呢debounce函数在第一次点击按钮就调用了,执行完后就从调用栈出去了,给里面的fuctioin留下了一个小背包也就是闭包,里面存有timer变量供fuctioin使用,那么这时,我每次创建和清除的是不是一直都是闭包里面的timer呀,大功告成。

3. 让this指向正确,上面的代码有个问题,正常的按钮,触发事件里面的this是指向button按钮的,而我们

handle声明在全局,并且是独立调用,触发了this的默认绑定规则,让其指向了window。


正常的按钮:

<body>
  <button id="btn">提交</button>
  <script>
    let btn = document.getElementById('btn')
    function handle() {
      // 模拟请求
      console.log('提交',this);
    }
    btn.addEventListener('click', handle)
  </script>
</body>

如图:

我们加了防抖的按钮

如图:

我们通过call来把fn的this掰到button上面来:


 function debounce(fn) {
      let timer = null
      return function() {
        // 如果第二次的时间没到1s,就销毁上一次的定时器
        const _this = this
        clearTimeout(timer)
        timer = setTimeout(function(){
          fn.calll(_this)
        }, 1000)
      }
    }

点击按钮是触发debounce函数里面的function对吧,那么function里面的this就指向button咯,我们就把fn的this通过call方法掰弯到_this。


还有一种是箭头函数的情况:

function debounce(fn) {
      let timer = null
      return function() {
        // 如果第二次的时间没到1s,就销毁上一次的定时器
        clearTimeout(timer)
        timer = setTimeout(()=>{
          fn.call(this)
        }, 1000)
      }
    }

我们知道箭头函数里面是没有this,那么是不是相当于是setTimeout()括号里面的this,那就是这样咯setTimeout(this),就相当于setTimeout的形参呀,那么这个this又是在function函数里面,那它不就指向button吗,再用call方法掰过来就好了。

最后,还有事件参数e的问题,最后改进一下:

 function debounce(fn) {
      let timer = null
      return function(e) {
        // 如果第二次的时间没到1s,就销毁上一次的定时器
        const _this = this
        clearTimeout(timer)
        timer = setTimeout(function(){
          fn.calll(_this,e)
        }, 1000)
      }
    }

我们把function(e)的事件参数在call里面传给handel就好了。


function handle(e) {
      // 模拟请求
      console.log('提交',e);
    }

handel函数这里记得加一下参数.


最终效果:

至此真正的大功告成!


总结

别看防抖就这点代码,但是它涉及的知识点还是蛮多的,有this,有闭包,本文到此就结束了,我们实现了有个简单的防抖按钮,希望对你有所帮助,感谢你的阅读!