防抖与节流

8 阅读4分钟

防抖(debounce)

实现防抖的核心逻辑:只触发最后一次调用的函数。 将频繁触发的事件合并为一次去执行

应用场景:按钮提交、搜索

const debounce = (func, wait=50)=> {
	let timer = 0
	return function(...args){
		if(timer) clearTimeout(timer)
		timer = setTimeout(()=>{
			func.apply(this,args)
		},wait)
	}
}

每次调用防抖函数时,都会清除之前的定时器(clearTimeout(timer))。

然后重新设置一个新的定时器(setTimeout),在指定的 wait 时间后调用 func。

如果在 wait 时间内再次调用防抖函数,会重置定时器,从而延迟 func 的执行

timer

timer 是一个变量,用于存储 setTimeout 返回的标识符

在防抖函数中,timer 的作用是用来跟踪是否已经存在一个尚未完成的定时器。如果存在,就清除它(取消之前的延时执行),从而保证只保留最新的一次函数调用的延时执行

timer 的生命周期

timer = setTimeout(() => {
  console.log("Hello");
}, 1000);

setTimeout 返回的标识符存储到 timer 变量中。

在这 1000ms 的时间内,timer 表示这个定时器(比如:数字 1 或 Timeout 对象)。

如果 clearTimeout(timer) 被调用,就会取消这个定时器。

如果定时器触发(即到达 1000ms 后),timer 并不会自动变为 null,它仍然是之前的值

为什么需要手动管理 timer?

如果不手动管理 timer,就无法取消之前的定时器,从而失去防抖效果

const debounce = (func, wait = 1000) => {
  let timer = null; // 初始化为 null

  return function (...params) {
    if (timer) {
      // 如果已有定时器,清除它
      clearTimeout(timer);
    }

    // 创建新的定时器
    timer = setTimeout(() => {
      func.apply(this, params);
    }, wait);
  };
};

// 示例
const log = (message) => console.log(message);
const debouncedLog = debounce(log, 1000);

debouncedLog("Hello"); // 设置定时器
debouncedLog("World"); // 清除上一个定时器,设置新的
debouncedLog("Final"); // 再次清除旧的,设置新的

timer 的变化过程

时间点操作timer的值定时器状态
0ms调用 debouncedLog("Hello")标识符 1(定时器 ID)定时器开始计时
500ms调用 debouncedLog("World")标识符 2(新定时器 ID)旧定时器被清除
800ms调用 debouncedLog("Final")标识符 3(新定时器 ID)再次清除旧定时器
1800ms定时器触发,执行 func("Final")3定时器已完成

调用方式的说明

debounce 函数返回一个新的函数, 本质上是一个高阶函数

debouncedLog("Hello", "Alice") 是正确的用法,直接调用返回的防抖函数并传入参数

...params 收集了参数 ["Hello", "Alice"]

节流

目的:使得一定时间内只触发一次函数

核心逻辑:是通过 timer 判断是否允许执行函数,防止短时间内频繁触发

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

function throttle(func, delay) {
  let timer = 0; // 定义一个定时器标识符
  return function (...args) { // 使用扩展运算符接收参数
    if (timer) return; // 如果已有定时器在运行,直接返回
    timer = setTimeout(() => {
      func(...args); // 直接调用传递的函数
      timer = 0; // 清空定时器标识符
    }, delay);
  };
}
const throttledLog = throttle((message) => console.log(message), 2000);

throttledLog('Message 1'); // 第一次调用,输出 'Message 1'
throttledLog('Message 2'); // 第二次调用,直接返回
setTimeout(() => throttledLog('Message 3'), 2500); // 超过 2 秒后,输出 'Message 3'

执行过程分析

  1. 第一次调用

timer 初始值为 0

调用 throttledLog('Message 1')timer0,进入 setTimeout。定时器开始计时(delay = 2000 毫秒),输出 'Message 1'

  1. 第二次调用

在定时器未完成的情况下(2000ms 内),timer 不为 0

调用 throttledLog('Message 2')if (timer) 条件成立,直接 return。此时,'Message 2' 未被打印。

  1. 定时器触发

超过 2000ms 后,定时器触发:执行 func(...args)。将 timer 设置为 0,表示可以再次执行。

  1. 第三次调用

超过 2000ms 后(timer0),调用 throttledLog('Message 3'):再次设置新的定时器,输出 'Message 3'

关于this

  • 如果 func 使用了 this,需要显式保存上下文(const context = this)并通过 apply 传递。
  • 如果 func 是一个不依赖上下文的函数(如箭头函数),则可以直接使用 func(...args)

应用场景:

  • 拖拽元素时,触发高频的鼠标移动事件,可能导致性能问题
  • 在表单中监听用户输入并执行实时验证或搜索时,可能触发过多的函数调用
  • 用户滚动页面加载新内容时,需避免频繁的网络请求
  • 防止用户在短时间内多次点击按钮,触发多次操作(如提交表单、发送请求) -- 限制按钮的点击频率。
  • 浏览器窗口调整时触发大量的 resize 事件,导致页面性能下降