深入解析js防抖与节流

460 阅读5分钟

防抖

防抖是什么

我相信在日常生活中大家都遇到过这种场景,进入一家装有自动门的饭店恰饭,当自动门感应到有人靠近时会自动开门,隔一段时间后又会自动关上,如果期间又有人靠近,自动门关门的时间间隔会重新计算,这就是防抖在现实中的应用。这个案例中防抖的实现有三个核心要素:开门、延时关门、重新计时,在javaScript中如果要实现防抖,那就是利用事件监听、setTimeout、clearTimeout三者来组合实现,最终达成 事件在触发一段时间后执行回调函数,如果这期间事件又被触发,则重新计时 的效果。

整体思路

防抖的应用场景很广,我就使用表单提交时的防止多次点击按钮造成重复提交的例子。首先我们创建一个button按钮,在script标签中获取按钮并添加监听事件,创建一个回调函数并用setTimeout来为这个回调函数设置延时,在每次触发之前用clearTimeout清除延时重新计算,最后在监听事件中调用这个回调函数就完成了。

代码实现

  <button>剁手按钮</button>
  
  <script>
    const button = document.querySelector('button')

    const payMoney = function () {
      console.log('剁手成功');
      console.log(this);
    }

    function dobounce(fun, time) {
      let timer
      return function () {
        const that = this
        clearTimeout(timer)
        timer = setTimeout(() => {
          fun.call(that)
        }, time);
      }
    }

    button.addEventListener('click', dobounce(payMoney, 1000))

效果展示

fangdou_哔哩哔哩_bilibili

难点分析

虽然对于很多大佬来说,防抖节流也就洒洒水啦,但毕竟还是有小萌新嘛。

回调函数首次不自动执行

如果在button的监听事件中直接调用payMoney函数,则会发生还没有点击按钮函数就已经执行的情况,所以这里需要用到高阶函数,定义一个dobounce函数将payMoney函数return出来,这样就实现了不点击按钮函数就不执行的效果。

清除延时

要清除延时,我们就需要将整个setTimeout赋值给一个变量,这里我设置为timer,很显然,想要清除timer,就必须先定义timer,因此会有萌新写成这样:

  function dobounce(fun, time) {
      
      return function () {
        let timer
        const that = this
        console.log('已点击');
        clearTimeout(timer)
        timer = setTimeout(() => {
          fun.call(that)
        }, time);
      }
    };

这样表面上达到了setTimeout执行之前清除延时的效果,但实际效果是,延时结束后,延时期间的所有点击都生效了,这是因为每次点击执行函数之间都是独立的,每次点击都定义了一个新的timer,它们之间没有联系。要解决这个问题其实也简单,我们把timer的定义提到外层,利用闭包的特性,这样每次点击执行的函数访问到的都是同一个timer。

this指向

从表面上来看,防抖函数到这里就已经实现了,看不出有啥问题,但是如果我们打印payMoney函数中的this,会发现它指向window而不是button,这就是this丢失了,虽然表面上看起来没问题,但在实际开发中却会产生令人头疼又难以发现的bug,所以我们需要绑定this。我在setTimeout之前定义了一个that将this保存了起来,在setTimeout中使用call(that)实现绑定this的效果。到这里防抖函数才算完全实现了。

节流

节流是什么

节流的实现与防抖有许多相似之处,但思路不一样,防抖主要实现的功能就是事件在一段设定的时间内只能触发一次,期间多次触发就只生效一次

整体思路

这里我用统计页面大小的例子来展示节流,有很多跟节流重复的代码就不再描述了,需要注意的就是防抖需要进行一个条件判断,timer是否存在,如果timer存在(即被赋值了),就直接return掉,如果timer不存在,则函数执行,并将timer赋值为null,这里不用clearTimeout清除延时,而是直接将timer赋值为null。

代码实现

   function getSize() {
      let height = window.innerHeight;
      let width = window.innerWidth;
      let res = `页面的大小是${width}*${height}`
      console.log(res);
    }

    function throttle(fun, delay) {
      let timer;
      return function () {
        const that = this
        let args = arguments
        if (timer) return;
        timer = setTimeout(() => {
          fun.apply(that, args)
          timer = null;
        }, delay);
      }
    }

    window.addEventListener('resize', throttle(getSize, 2000))

效果展示

jieliu1_哔哩哔哩_bilibili

第二种实现方法

前面判断timer是否被赋值来决定函数是否执行,但我门还可以使用时间戳,即Date对象来实现这一功能。

思路

这里的 if 判断改为判断现在的时间减去上次发生的时间是否大于设定的时间间隔,如果大于则执行函数。我将初始时间设置为0,则函数首次触发就没有延时。

代码

  function getSize() {
      let height = window.innerHeight;
      let width = window.innerWidth;
      let res = `页面的大小是${width}*${height}`
      console.log(res);
    }

    function throttle(fun, delay) {
      let time = 0;
      return function () {
        let now = new Date()
        const that = this
        let args = arguments
        if (now - time > delay) {
          time = now;
          fun(that, args)
        }
      }
    }

    window.addEventListener('resize', throttle(getSize, 2000))

效果展示

jieliu2_哔哩哔哩_bilibili

节流总结

节流还有很多种写法,但都是根据这两种进行变化的,掌握这两种基本的实现方法,其他的方法就能做到一看就会,在实际开发中我们可以根据不同的需求采取不同的写法。

结语

  • 防抖和节流都是实际开发中为了防止事件频繁触发而产生资源浪费采取的策略,但它们的实现思路和效果又各不相同,
  • 防抖让事件在规定时间内只能触发一次,而节流让事件间隔一定时间触发。
  • 写文章不易,点个赞再走吧

参考链接

[](7分钟理解JS的节流、防抖及使用场景 - 掘金 (juejin.cn))

[](JavaScript 节流 - Web前端工程师面试题讲解_哔哩哔哩_bilibili)