前端性能优化之——防抖(Debounce)与节流(Throttle)

1,365 阅读5分钟

一、防抖 (Debounce)

1、什么是防抖?

防抖指的是在一段时间内,多次触发相同事件时,只执行最后一次触发的事件。也就是说,在一系列触发事件中,如果在指定的时间间隔内,发生了新的触发事件,那么前面的触发事件将被忽略,只有最后一次触发事件会被执行。

2、防抖的适用场景

防抖会延迟响应,适用于可等待的场景,如:

  • 窗口调整 resize: 当窗口大小发生变化时,会频繁的触发 resize 事件,可以使用防抖来延迟执行某些操作,例如在调整结束后再重新计算布局或重新渲染页面。

  • 防止误触: 例如支付按钮,需确保用户停止点击后才执行一次操作‌,避免重复执行引发错误。

  • 搜索输入框: 当用户在输入框输入内容时,会连续触发 keyup 事件,可以使用防抖来延迟触发搜索:只有在用户停止输入后才会发送请求,避免频繁请求,降低服务器压力。

  • 表单验证: 如手机号、邮箱格式校验,可以在用户停止输入后再进行校验,降低校验事件触发频率。

3、如何实现防抖?

// 防抖函数,func为需要防抖的函数,delay为延迟执行的时间
const debounce = (func, delay) => {

  // 创建一个timeout变量,用于存储setTimeout的返回值
  let timeout;

  // 返回一个函数,该函数接受参数args,func在每次调用时都会执行防抖逻辑
  return function (...args) {
    // 保存当前函数的上下文
    const context = this;

    // 清除之前的定时器,确保只有在delay后才执行一次func
    clearTimeout(timeout);

    // 设置一个新的定时器,在delay后执行func,并传入之前保存的args和context上下文
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, delay)
  }
}

如果防抖需要立即执行,可以添加immediate参数来控制


// 立即执行防抖函数,func为需要防抖的函数,delay为延迟执行的时间,immediate为是否立即执行
const debounceImmediate = (func, delay, immediate) => {

  // 创建一个timeout变量,用于存储setTimeout的返回值
  let timeout;

  // 返回一个函数,该函数接受参数args,func在每次调用时都会执行防抖逻辑
  return function (...args) {
    // 保存当前函数的上下文
    const context = this;

    // 确保timeout不为null
    if (timeout) clearTimeout(timeout);

    // 如果immediate为true,则立即执行func,并设置timeout为null
    if (immediate) {
      const callNow = !timeout; // 第一次会立即执行,之后不会
      timeout = setTimeout(() => {
        timeout = null;
      }, delay);

      // 如果callNow为true,则立即执行func
      if (callNow) func.apply(context, args);
      
    } else {
      // 如果immediate为false,则延迟执行func,并设置timeout为null
      timeout = setTimeout(() => {
        func.apply(context, args);
      }, delay);
    }
  }
}

二、节流 (Throttle)

1、什么是节流?

节流指的是在一段时间内,多次触发相同事件时,只执行一次事件。也就是说,无论触发事件发生多少次,在指定的时间间隔内只会执行第一次事件。

2、节流的适用场景

节流会即时响应,适用于需要周期性反馈的场景,如:

  • 页面滚动: 当用户滚动页面时触发 scroll 事件,可以使用节流控制执行scroll处理函数的时间间隔,减少执行次数。

  • 按钮点击: 例如游戏中的技能按钮,需立即响应第一次点击,但限制后续点击频率‌。

  • 鼠标移动: 当用户移动鼠标时,可以使用节流限制鼠标移动事件 mousemove 的触发频率,避免过多的计算和页面渲染

  • 动画帧率: 例如每秒60帧渲染,通过节流确保操作按固定间隔触发,保持流畅性‌

  • 通信限制: 在用户发送消息时,可以使用节流限制在规定时间间隔内发送消息的次数,降低服务器压力

3、如何实现节流?

(1) 定时器写法

  • 延迟执行
  • 尾触发:停止触发后会再执行一次
// 延迟执行 节流函数
const throttle1 = (func, delay) => {
  let timeout = null
  return function (...args) {
    const context = this
    if (!timeout) {
      timeout = setTimeout(() => {
        func.apply(context, args)
        timeout = null
      }, delay)
    }
  }
}

原因:

  • 在事件频繁触发时(如滚动中),timeout 是非空的,func 不会被执行;
  • 直到某次触发后,setTimeout 被设置,delay 毫秒后 func 执行
  • 如果这时停止触发,那么这最后一次被 setTimeout 包裹的调用仍然会执行
  • 所以它会“补上”最后一次执行。

(2) 时间戳写法

  • 立即执行
  • 首触发:停止触发后不会再执行
// 立即执行 节流函数
const throttle2 = (func, delay) => {
  let oldTime = Date.now();
  return function (...args) {
    let newTime = Date.now();
    if (newTime - oldTime >= delay) {
      func.apply(this, args);
      oldTime = Date.now();
    }
  }
}

原因:

  • 每次触发事件时都会判断当前时间和上次执行时间的间隔;
  • 如果间隔未达到 delay直接跳过,什么都不执行
  • 一旦停止触发,函数不会再有机会执行,因为不会再触发时间间隔的判断;

三、防抖与节流的区别

举个🌰:

  • 节流就像是首发站的公交车,每固定时间间隔发车
  • 防抖就像电梯门,当有人进入,会等待某时间后关门,如果等待中途,又有人进入,则重新开始计时
特性防抖节流
执行时机最后一次操作停止后执行固定间隔内最多执行一次
执行次数事件触发后可能只执行一次(若事件没有被再次触发)在事件持续触发期间,会根据固定间隔多次执行
适用场景输入框搜索、窗口resizescroll、mousemove