前端优化双胞胎:防抖和节流

848 阅读9分钟

前言

在前端开发的世界里,我们常常会遇到一些喜欢“抖腿”的问题。按钮被疯狂点击,函数被频繁调用,网络请求被狂轰滥炸……这些问题让我们的代码看起来像是在“抖腿”,让用户体验像是在坐过山车。今天我们要来谈谈如何让你的代码从“抖腿”变成“稳如老狗”。我们要介绍一对双胞胎:防抖和节流。别看它两名字不一样,但是他们都是是我们代码中的“稳定剂”,可以让我们的应用程序更加流畅,更加高效,更加可靠。

1. 防抖(debounce)

顾名思义,这个功能就是防止抖动的,那在前端的世界中抖动是什么呢?场景有用户频繁的发送请求等等,这种就像在上课时同桌疯狂抖腿一样让人不厌其烦,为了避免用户频繁发送请求,所以我们就有了防抖这个功能。

image.png

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

1.1 基础版防抖

在了解完了防抖的概念之后,接下来我们来实现一下这个功能,根据它的概念我们可以得知,只需要在触发事件时判断一下用户是否在规定时间内已经触发了一次事件,这就需要我们的定时器setTimeout了,下面我们直接来看代码实现:

定时器执行完之后本质上在,但是在垃圾回收机制执行完后(啥时候执行不知道,所以默认执行完后会被销毁)会被回收销毁。

<!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.querySelector('#btn')

    function handle(e) {
      console.log('向后端发送数据');
    }// 防抖函数执行的业务逻辑

    btn.addEventListener('click', debounce(handle, 1000))

    // 防抖函数,传入业务逻辑和时间
    function debounce(fn, wait) {
      let timer = null// 产生闭包用来储存定时器
      return function () {
        if (timer) clearTimeout(timer)// 用来清除前一个定时器
        timer = setTimeout(() => {
          fn()
        }, wait)
      }
    }
  </script>
</body>
</html>
PixPin_2025-01-05_16-43-26.gif

根据上面展示的效果我们可以知道,当我们频繁点击的时候,只有最后一次点击才有效果。为了实现这个功能在上面代码中我们可以使用闭包timer来储存上一次定时器,在下一次点击事件触发时,我们会判断一下是否有定时器存在,如果有则清除,否则就设置新的定时器,在定时器内执行我们到了事件想要执行的业务逻辑。

这时候可能就有同学会疑惑了,这防抖听起来好像挺牛逼的,但是看起来好像就是那么回事。其实上面这个代码只是一个丐版的防抖。

俗话说有舍必有得,我们虽然实现了这个功能,但是也丢失了一些东西,比如业务逻辑函数中的this和本身自带的事件参数e,接下来我们还是用上面的代码,只修改js部分来看看fn中的thise分别打印的是什么:

事件参数e:

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

    let btn = document.querySelector('#btn')
  
    function handle(e) {
      console.log('向后端发送数据');
      console.log(this);
      console.log(e);
      
    }// 防抖函数执行的业务逻辑

    
    btn.addEventListener('click', debounce(handle, 1000))

    // 防抖函数,传入业务逻辑和事件
    function debounce(fn, wait) {
      let timer = null
      return function () {
        if (timer) clearTimeout(timer)// 用来清除前一个定时器
        timer = setTimeout(() => {
          fn()
        }, wait)
      }
    }
PixPin_2025-01-05_17-05-06.gif

本来呢按理来说this的指向应该是谁调用就指向谁,e也是自身发生的事件参数,在这里呢我们的fn应该是指向btn的,e也应该有数据。但是我们在时间到了执行fn()的时候它this的打印结果是Window,e的结果是undefined。这就代表它并没有被谁调用,所以指向了最外层的Window(有不理解this指向的小伙伴可以看this的执行机制)。

1.2 豪华版防抖

正所谓便宜没好货,虽然实现了这个功能但是丢失了一些我们身上该有的东西,现在我们应该把它拿回来,这时候我们就可以用call或者apply这种函数把fn身上的this强制指回btn,并且能够传入事件参数e,正所谓强扭的瓜不甜,但是它解渴嘛。接下来我们来看看代码实现,只修改debounce

    function debounce(fn, wait) {
      let timer = null
      return function (e) {
        if (timer) clearTimeout(timer)// 用来清除前一个定时器
        timer = setTimeout(() => {
          fn.call(this, e)// 将fn中的this指向btn,并传入参数e
        }, wait)
      }
    }
PixPin_2025-01-05_17-08-27.gif

在这里呢可能就有小伙伴觉得我身上的thise也已经回来了,现在应该差不多了吧。咱们干事情呢得尽善尽美,虽然看起来还可以,但是我们在实际开发的时候总不能一直让业务逻辑的函数只传入事件参数e,不传入别的参数吧,这时候我们就可以用es6的新语法剩余参数来解决了,下面我们来看完整版js代码:

    let btn = document.querySelector('#btn')

    function handle(e) {
      console.log('向后端发送数据');
      console.log(this);
      console.log(e);
    }// 防抖函数执行的业务逻辑

    btn.addEventListener('click', debounce(handle, 1000))

    // 防抖函数,传入业务逻辑和事件
    function debounce(fn, wait) {
      let timer = null
      // 这个e就是原本的事件参数,e会存在于args中
      return function (...args) {
        if (timer) clearTimeout(timer)// 用来清除前一个定时器
        timer = setTimeout(() => {
          fn.call(this, ...args)// 将fn中的this指向btn并且将事件参数改为还是fn的
        }, wait)
      }
    }

防抖注意事项

  1. 要有防抖效果
  2. 不能修改原函数的this指向
  3. 不能影响原函数的参数(例如事件参数e)

2.节流(throttling)

在了解完了它的小老弟防抖之后,接下来我们来聊聊节流,这两哥们为什么说它们是双胞胎呢,这是因为它两实现的功能其实都差不多的,都是为了防止在一段时间内用户请求多次而产生的,接下来我们来了解一下节流。

节流概念:在规定时间内如果多次触发的话,只执行一次

节流的概念应该不难理解,就是在规定时间内如果用户多次发送请求的话只会发送第一次,剩下的都不发送,当时间到了之后如果还触发的话才会开始第二次请求的计时,依次类推,下面我们来看一下实现效果:

PixPin_2025-01-08_20-31-08.gif

接下来我们来看一下代码实现,都说了它跟防抖是双胞胎,所以它也有基础款和豪华款,接下来我们来对它的两个版本一一了解一下

2.1 基础版节流

在我们来看代码之前我们先来聊聊它的大致思路:首先我们肯定要像防抖一样弄个闭包用来储存一个变量用来储存时间戳看看时间是否到达,每次触发事件时如果时间小于wait就啥也不干,如果大于wait的话就执行操作然后重新设置变量,接下来我们来看看代码实现:

<!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(e) {
      console.log('向后端发送请求');
    }

    btn.addEventListener('click', throttle(handle, 1000))

    function throttle(fn, wait) {
      let preTime = null
      return function () {
        // 获取当前时间
        let nowTime = Date.now()
        //判断当前事件减去之前定时器的事件是否大于等待时间
        if (nowTime - preTime > wait) {
          fn.apply()
          preTime = nowTime
        }   
      }
    }
  </script>
</body>
</html>
PixPin_2025-01-08_20-38-02.gif

在上面的代码中,我们使用了一个Date.now()用来获取当前时间,而这个函数的结果是什么呢,我们来浏览器打印一下:

image.png

时间戳:一种用于表示特定时间点的数字表示法,通常以毫秒为单位,记录从1970年1月1日00:00:00 UTC(协调世界时)开始到当前时间的秒数。这种表示法在计算机系统和编程中被广泛使用,以便于记录和比较时间。

我们可以看到它打印出来的是一个时间戳,并且是我们当前时间的时间戳。而时间戳是只增不减的,所以我们只需要每次触发事件时,就获取触发事件时的时间戳减去上次触发储存的时间戳就能获取相邻两次触发事件的时间间隔。如果大于等待时间则对preTime重新赋值新的时间戳,否则就啥也不干。

我们现在可以看到功能基本实现了,好像也挺简单的,但是都说了双胞胎,它俩肯定心有灵犀的,所以它同样也丢失了this和e,不信的话我们在handle中分别打印this和e试试:

PixPin_2025-01-08_20-49-43.gif
2.2 豪华版节流

我们根据上文的效果可以看到想实现基础功能的话非常简单,就是设置个闭包拿来存放事件即可。但是也和防抖一样丢失了两样东西thise,我们如果想要拿回它们两个的话方法同豪华版防抖一样,只需要用apply给它绑定即可,下面我们来看看js代码部分:

<script>
    let btn = document.getElementById('btn')

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

    btn.addEventListener('click', throttle(handle, 1000))

    function throttle(fn, wait) {
      let preTime = null
      return function (e) {
        let nowTime = Date.now()
        if (nowTime - preTime > wait) {
          fn.apply(this, e)
          preTime = nowTime
        }   
      }
    }
  </script>
PixPin_2025-01-08_20-53-04.gif

我们可以看到通过apply()给它强行掰直后,也是终于修成正果了,这时候我们看起来确实非常完美了,但是还是有一点点小瑕疵,就如同防抖一样如果执行事件的回调函数中要传入参数的话,我们就得需要用剩余参数来接收了,下面我们来看完整js代码(html其余部分同上):

<script>
    let btn = document.getElementById('btn')

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

    btn.addEventListener('click', throttle(handle, 1000))

    function throttle(fn, wait) {
      let preTime = null
      return function (...args) {
        let nowTime = Date.now()
        if (nowTime - preTime > wait) {
          fn.apply(this, args)
          preTime = nowTime
        }   
      }
    }
</script>

结语

关于前端优化的双胞胎——防抖和节流的介绍就到这里!记住,防抖是那个“慢性子”,它会帮你减少不必要的事件触发,而节流是那个“调节师”,它会确保你的应用始终保持在最佳的运行状态。因此,在实际开发中灵活地运用防抖和节流,将有助于我们打造更加流畅的前端应用。

1732338928918.jpg