理解闭包理解防抖

本文最后更新于:2024年10月26日 晚上

防抖

防抖主要应用在一些高频次操作事件上,比如:scroll 滑动、input 输入、resize 缩放等。

什么是防抖

防抖的意思是在高频率触发的事件中,只执行最后一次操作,前面的操作都会被取消掉。例如,你在页面中连续滚动,防抖函数会等待一定的时间,如果这段时间内没有再次触发滑动,才执行实际的处理逻辑。如果在等待的时间内事件再次触发,计时器就会被重置,直到没有再触发时,才执行一次操作。

一般来说防抖函数形式如下

1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) { // fn为传入的函数名
  let timer = null;  // 用来保存定时器的 ID
  return function(...args) {  // 返回一个闭包函数
    if (timer) clearTimeout(timer);  // 若已有定时器则清除它
    timer = setTimeout(() => {  // 设置一个新的定时器
      // 当延时结束时,执行传入的函数,并绑定当前的上下文和参数  
      fn.apply(this, args);
    }, delay);  // delay 是等待的时间,单位是毫秒 ms
  };
}

使用示例

假设我们有一个函数 handleScroll,它在页面滚动时处理一些操作,我们不希望它每次滚动都触发,因为这会增加不必要的性能消耗,我们希望它在滚动停止后执行,那么可以这样使用防抖函数

1
2
3
4
5
6
7
8
9
10
function handleScroll() {
    console.log("处理滚动事件中...");
  	// 实际逻辑略
}

// 使用防抖包装滚动事件处理函数,等待300毫秒
const debouncedHandleScroll = debounce(handleScroll, 300);

// 当页面滚动时
window.addEventListener('scroll', debouncedHandleScroll);

理解防抖函数

为理解防抖函数,首先要看看防抖函数怎么被使用。由上面的使用示例可以知道,调用时,debounce 的第一个参数为我们希望进行管理的高频次响应或处理事件函数,第二个参数为我们希望的连续事件结束后等待的时间。

一进入防抖函数,首先检查是否存在过定时器,如果有则清除,如果没有则新建一个定时器,并在定时器内调用回调,执行真正你想执行的操作。

核心是:多次触发事件时,只有最后一次触发后的延迟时间结束后才执行目标函数

  • 什么是 timer?

在上面的页面滑动例子中,timer 在每次被调用的时候被赋值为定时器的 ID

具体来说,setTimeout 的返回值是一个定时器的标识符 ID。而定时器的 ID 的类型因环境不同也有所差异:在浏览器环境中,setTimeout 的返回是一个数值类型的 ID,例如第一次调用可能返回 1,第二次为 2,依次递增;在 Node.js 环境中,setTimeout 返回值可能是一个定时器对象,但仍然可以通过 clearTimeout 消除。当然也可以将 timer 写作别的名称,只要合法都行,但为了直观还是建议写成 timer

  • 为什么每次开始要判断是否清除定时器 ID?

因为定时器 ID 的存在状态标识了某事件是否还在发生。

假设每次开始不清除定时器,那么每一次滑动页面都会设置一个新的定时器,这样并不能做到防抖效果,因为你只是将每一次的滑动进行了延时,最终每一次的滑动处理事件都将发生。

现在,假设每次开始都清除定时器,那么当前的滑动事件会将上一次的滑动事件定时清除,并为自己这一次的滑动事件进行定时,而这一次若为最后一次,函数将不再会被调用,也就不再会清除定时器,这一次的事件将会被正确的以设置好的时间定时进行处理。

这种事件可以类比生活中的乘电梯事件:将电梯门看作事件处理函数,进入电梯的这一动作看作不希望的高频次事件。当人按下电梯按钮,电梯抵达并开门,开门的同时开始计时,每有一个人被红外检测到,人经过电梯门进入电梯之后,电梯门的计时就重新开始(清除上一次的计时),直到没有人进入(高频次事件停止,进入电梯的最后一个人为最后的事件),电梯门最终关闭(定时器时间到了),电梯执行上升或下降等约定好的操作(执行回调函数)。

  • 为什么不将 timer 写在外部?为什么返回一个闭包函数?

timer 在功能上看,当然可以作为全局变量定义在整个函数外部,但是这样一来如果别处也要使用 timer 就会显得不太合适,timer 会被污染,不便于函数的封装复用并且还存在安全问题,将其私有化是一个好的选择。

而且闭包可以记录外部变量的状态(这主要依靠作用域链),这样能保证多次触发事件访问到的是同一个timer,保证多次事件触发能清除到前一个事件的定时,最终实现防抖。

  • what the hell is this closure anyway? 这闭包到底是啥?

闭包(closure)是 JavaScript 中一个重要的概念,闭包是指一个函数可以访问它外部函数作用域中的变量,即使在外部函数已经执行结束后,它仍然可以通过引用这些变量。

什么意思呢?假设有这样一个函数

1
2
3
4
5
6
7
8
9
10
11
12
function createCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    };
}

const counter = createCounter();
console.log(counter());  // 输出 1
console.log(counter());  // 输出 2
console.log(counter());  // 输出 3

按理说一个函数 return 之后函数本身就已经 over 了,但是 counter 还是能够访问到 count 的值,并进行了累加。类似这样的一个返回的函数就是闭包。闭包一般是在函数嵌套的情况下形成的。当一个函数 A 返回另一个内部函数 B,并且 B 可以访问 A 函数内部定义的变量时,闭包就形成了。

  • ...args 是什么?为什么?

在 JavaScript 中,...args 是一种称为 剩余参数(Rest Parameters) 的语法,它用于将函数的不定数量的参数收集到一个数组中。这样可以让函数接受任意数量的参数,并将它们组合在一起,便于操作和处理。

说到为什么要使用它,还得说说它的好处。假设有这样一个函数

1
2
3
4
5
6
7
function logArguments(...args) {
  console.log(args);  // args 是一个数组,包含了所有传入的参数
}

logArguments(1, 2, 3);   // 输出 [1, 2, 3]
logArguments("a", "b");  // 输出 ["a", "b"]
logArguments();          // 输出 []

不论传入多少参数,有还是没有,它都能够接收并存储在 args 数组中。

那为什么防抖中要用到呢?

一来是为了使得函数能够处理任意数量的参数,使其更通用;二来是为了能够正确地传递参数。可别不以为意,有时候还真没多少人能正确地传递参数。


理解闭包理解防抖
https://4rozen.github.io/archives/Vue/35829.html
作者
4rozeN
发布于
2023年7月6日
许可协议