这是比一般防抖,多一点优秀的"二班"防抖!

1,623 阅读8分钟

防抖

防抖是什么

举个例子:

这段代码是一个简单的HTML页面,其主要功能是在一个高度为200像素的蓝色矩形容器中显示一个数字,并且每次鼠标移动在容器内时,数字会递增。

当用户使用鼠标或触摸屏时,可能会出现一个问题:尽管他们只是轻轻移动了一下鼠标或手指,但屏幕上的数字却在飞速增加。这种现象就像我们的手在微微颤动,却造成了屏幕上的数字剧烈变化一样。这个问题会导致用户感觉操作不准确,降低了用户体验。为了解决这个问题,就有"防抖"这个概念。

而且当事件处理函数不再仅仅执行简单的+1操作时,例如涉及到耗时的任务>触发频率的情况,会导致页面性能下降,甚至出现卡顿现象。这是因为在短时间内频繁触发事件会导致浏览器执行大量的JavaScript代码,占用了大量的CPU资源,影响了页面的响应速度和用户体验。为了解决这个问题,可以使用"防抖"技术来优化前端性能。

防抖的作用

防抖的核心思想:在事件被触发后,延迟一定时间再执行事件处理函数,如果在这段时间内又触发了相同的事件,则销毁上一次,重新计时。

防抖带来的效益:

  1. 减少不必要的事件触发:在用户执行某些高频率操作时(例如窗口大小调整、滚动、键盘输入等),防抖可以确保在特定时间段内事件只会被触发一次,从而减少多次触发带来的资源浪费。
  2. 提高性能:频繁触发事件会导致程序反复执行一些计算密集型操作,如重新渲染页面、发送网络请求等。防抖技术通过减少事件的触发频率,降低了CPU和内存的占用,从而提高程序的整体性能。
  3. 改善用户体验:过于频繁的响应用户操作可能会导致界面卡顿或反应迟缓,影响用户体验。防抖技术可以使应用程序的响应更加平滑和稳定,提升用户满意度。

防抖实现原理

防抖的工作原理:设置一个定时器,当事件触发时,启动定时器。如果在定时器到期之前,事件再次触发,则重置定时器。只有在定时器到期后,事件处理函数才会被实际执行。

所以针对上面的例子js应该写成这样:

简易版:

var count = 1;
var container = document.getElementById('container');

// 事件处理函数
function getUserAction() {
  container.innerHTML = count++;
}

//防抖函数
function debounce(func, delay) {
  let timer;

  return function() {
    clearTimeout(timer);
    
     // 这里的func函数里的this -> 作为函数独立调用,所以this -> window
    timer = setTimeout(func, delay);
  };
}

container.onmousemove = debounce(getUserAction, 1000); // 在mousemove事件上应用防抖

上述代码可以实现一个简单的防抖,但是整体性能不够健壮,存在一些问题:

  1. 上下文(Context)的丢失问题: getUserAction函数在被调用时,可能会丢失其原始的上下文(即this指向)。
  2. 传递参数问题: getUserAction函数无法接收到事件参数。

进阶版:

var count = 1;
var container = document.getElementById('container');

// 事件处理函数
function getUserAction() {
  container.innerHTML = count++;
}

function debounce(func, delay) {
  let timer;

  return function() {
    clearTimeout(timer);
     // 这个函数里的this 指向调用他的对象 也就是container
    const context = this; 
    const args = arguments;
    timer = setTimeout(function() {
      func.apply(context, ...args);//
    }, delay);
  };
}

container.onmousemove = debounce(getUserAction, 1000); // 在mousemove事件上应用防抖
  1. 在进阶版中,通过保存当前的上下文(context),并使用func.apply(context, ...args)来调用原始函数,确保了getUserAction函数在执行时依然具有正确的上下文。

  2. 在进阶版中,通过保存arguments,并在调用原始函数时使用...args来传递参数,确保了getUserAction函数能够正确接收到事件参数。

timer是什么

timer 是一个用于存储定时器 ID 的变量。具体来说,timer 的数据类型是 number(在浏览器环境下),它表示一个由 setTimeout 返回的标识符,可以用来控制延时操作。

timer 的作用 :

  1. 存储定时器 ID:timer 存储 setTimeout 的返回值,用于唯一标识这个定时器。每次调用 debounce 返回的函数时,都会重新设置一个定时器,并将新的定时器 ID 赋值给 timer。

  2. 清除之前的定时器:调用 clearTimeout(timer) 可以取消先前设置的定时器。如果在 delay 时间内再次触发事件,这个定时器会被清除,从而避免调用 func。

  3. 延时执行函数:在事件触发时,设置一个新的定时器,延时执行传入的函数 func。只有在 delay 时间段内没有再次触发事件时,定时器到期,才会调用 func。

为什么改变this指向

this指向问题可以看看这篇文章:juejin.cn/post/736660…

  1. 因为事件处理函数 getUserAction 需要正确的 this 引用来操作 container 元素的 innerHTML 属性。如果不处理好 this,在防抖函数中调用 getUserAction 时,可能会丢失正确的上下文,从而导致错误。

  2. apply 方法允许我们显式地设置函数执行时的 this 值和参数。通过 apply 方法,我们可以确保防抖函数在调用原始函数 func 时,能够使用正确的 this 值和参数!

为什么说这种方式使用了闭包

闭包是指函数能够记住并访问其词法作用域,即使当该函数在其词法作用域之外执行时。闭包使得内部函数可以访问外部函数的变量和参数。

当调用 debounce 函数时,timer 被创建并初始化。然后,debounce 返回一个新的函数。这个新函数就是一个闭包,因为它捕获了 debounce 的词法环境,包括 timer 变量。

即使 debounce 函数执行完毕并返回,timer 变量依然存在于返回的新函数的闭包中。当调用返回的新函数时,它可以访问和修改 timer

上面进阶版的"防抖"已经满足了合格要求,但是那只是一般的的防抖,那什么才是让大厂面试官眼前一亮的"二班"防抖呢?我们来看看

"二班"防抖

  1. 立即执行选项:有时候,我们可能希望防抖函数在第一次触发时立即执行,而不是等待延迟时间。为此,可以添加一个选项immediate来控制是否立即执行函数

  2. 返回值:这个防抖函数目前没有返回任何值。如果需要返回原始函数的执行结果,可以将其保存下来并在适当的时候返回。

所以为了满足这些需求。"二班"防抖应该这样写:

function debounce(func,delay, immediate) {
      //自由变量空间
      var timer, result

      //真正执行的函数
      return function () {
        // 二传手
        var context = this
        var args = arguments

        if (timer) clearTimeout(timer);
        if (immediate) {
          var callNow = !timer

          //为了一段时间时后下一次触发时能够继续立即执行第一次
          //为实现从一段时间内只执行最后一次——>从一段时间内只执行第一次
          timer = setTimeout(function () {
            timer = null //垃圾回收机制
          }, delay)

          if (callNow) result = func.apply(context, args)
        } else {
          timeOut = setTimeout(function () {
            result = func.apply(context, args)
          }, delay)
        }
        return result;
      }
    }

这段代码中的 debounce 函数接受三个参数:func 是要执行的函数,delay 是延迟的时间(毫秒),immediate 是一个布尔值,表示是否立即执行函数。函数内部定义了一个 timer 变量和一个 result 变量,用来存储定时器和函数执行的结果。

当调用 debounce 返回的函数时,会判断 immediate 是否为 true,如果是,会立即执行一次函数并设置一个定时器,在延迟时间结束后将定时器设为 null

为什么在延迟时间结束后将定时器设为 null

  1. 延迟重置 timer: 这个定时器在 delay 时间后执行,并将 timer 置为 null。这样做是为了在这段时间内防止立即执行(immediate)的功能被重复触发。如果在 delay 时间内再次调用防抖函数,因为 timer 仍然存在并未被置为 null,因此不会立即执行,而是等待 delay 时间后再执行。

  2. 实现防抖的效果: 在 immediate 模式下,当第一次调用防抖函数时,timernull,因此会立即执行并设置一个新的定时器。在 delay 时间内,如果再次调用防抖函数,由于 timer 仍然有效,会重置定时器并不会立即执行。只有当 delay 时间结束后,timer 被置为 null,下一次调用防抖函数时才会再次立即执行。这确保了在 delay 时间内只会有一次立即执行,而不是每次触发都立即执行。

让我们具体看一下 immediatetrue 时的工作流程:

  1. 第一次调用防抖函数:

    • timer为 null。

    • callNow 被设置为 true(因为 !timer 为 true)。

    • 执行函数 func 并设置 timer:

      timer = setTimeout(function () {
        timer = null;
      }, delay);
      
  2. delay 时间内再次调用防抖函数:

    • timer 不为 null。

    • 清除现有的定时器 clearTimeout(timer) 并设置一个新的定时器:

      timer = setTimeout(function () {
        timer = null;
      }, delay);
      
    • callNow被设置为 false(因为 !timer 为 false)。

    • 因此不会立即执行 func。

  3. 经过 delay 时间后:

    • 定时器执行,将 timer 置为 null。
  4. 再次调用防抖函数:

    • timer 为 null。
    • callNow 被设置为 true(因为 !timer 为 true)。
    • 再次立即执行函数 func。

总结起来,这个 setTimeout 操作的关键作用是确保在 delay 时间内,函数只能立即执行一次(如果 immediatetrue),并在 delay 时间后重置 timer 以便下一次调用可以再次立即执行。

最后

如果你学会了"二班"防抖,那么咱们就不是一般人了!

image.png