防抖与节流

1. 概念与区别

(1) 防抖

定义:在事件被触发 delay 毫秒后才执行。如果在该时间内再次触发,则重新计时

场景

  • 用户在输入框搜索(停止输入后再发请求)。
  • 自动保存(停止编辑后再保存)。
  • 窗口调整 resize (调整过程中不计算,停止后计算)。

核心思想:对于连续的事件,只执行最后一次

(2) 节流

定义:规定在单位时间内(如 1 秒)只能触发一次。如果该时间内被触发多次,只有一次生效。

场景

  • 滚动条 scroll 监听(可设置每 100ms 计算一次位置)。
  • 按钮防止重复点击。

核心思想:对于连续的事件,稀释执行频率(每隔多少时间执行一次)。

2. 手写防抖与节流

面试时,不仅需要说出区别,更需要现场手写代码。

(1) 手写防抖

/**
 * @param {Function} func 需要防抖的函数
 * @param {Number} delay 延迟时间 (ms)
 * @param {Boolean} immediate 是否立即执行一次
 */
function debounce(func, delay, immediate = false) {
  let timer = null;

  return function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate && !timer) {
      func.apply(this, args);
    }

    timer = setTimeout(() => {
      if (!immediate) func.apply(this, args);
      timer = null; // 执行完置空,下次可再次触发 immediate
    }, delay);
  };
}

(2) 手写节流

这里提供两种方案:时间戳版(立即执行,但在最后一次后不会再执行)和 定时器版(延迟执行,但在最后一次后会补执行)。面试写出简单版即可。

组合版 (推荐)

/**
 * @param {Function} func 需要节流的函数
 * @param {Number} wait 间隔时间 (ms)
 */
function throttle(func, wait) {
  let timer = null;
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();
    // 下次触发剩余时间
    const remaining = wait - (now - lastTime);

    // 如果时间间隔到了,或者系统时间被调回去了
    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = now;
      func.apply(this, args);
    } else if (!timer) {
      // 还在时间内,设置定时器在剩余时间结束后执行(补刀)
      timer = setTimeout(() => {
        lastTime = Date.now(); // 注意这里更新时间
        timer = null;
        func.apply(this, args);
      }, remaining);
    }
  };
}

3. 面试考点

  1. this 指向问题:回调函数必须使用 function 关键字或者箭头函数通过 apply 绑定正确的 this
  2. 参数透传:使用 ...args 并通过 apply 传递给原函数。
  3. 立即执行:防抖和节流都可以通过 immediate 选项控制首次是否立即触发。
  4. 取消功能:在返回的函数上挂载 .cancel() 方法清除定时器。

4. 总结

  • 防抖:最后一次说了算。(输入框搜索)
  • 节流:按节奏执行。(滚动监听)