JS函数防抖与节流的具体实现代码

作者:安全边界时间:2026-11-03 14:02:35浏览:301

深入解析JavaScript函数防抖(Debounce)与节流(Throttle)的具体实现代码,包括基础版、支持立即执行、支持取消方法的高阶封装,结合SVG图解与实际场景选择指南。

前端技术 JavaScript 代码实现与原理
JS函数防抖与节流的具体实现代码
在前端日常交互与页面渲染中,scroll、resize、mousemove 或 input 等高频事件可能在一秒内触发数十甚至上百次。如果绑定的处理函数涉及复杂的计算、DOM 操作或网络请求,主线程将承受巨大压力,直接导致页面掉帧、卡顿甚至接口雪崩。防抖(Debounce)与节流(Throttle)正是解决该问题的两项核心控制技术:防抖的核心是“等操作停下来再执行”,即只有在事件停止触发一定时间后才执行一次;节流的核心是“按固定节奏执行”,即在持续高频触发中保证按照预设时间间隔均匀执行。本文将从原理推演、逐步进阶的手写实现代码到实际场景选型,全面拆解防抖与节流的落地方案。

一、高频事件的性能瓶颈与解决思路

浏览器的事件循环在接收到硬件级别的输入事件(如鼠标移动、滚轮滚动)时,会在每一轮事件循环中将事件回调推入任务队列。现代显示器普遍具备 60Hz 甚至 120Hz 的刷新率,如果一个滚动监听回调需要 10ms 来查询 DOM 尺寸与重排计算,连续触发就会迅速耗尽每帧 16.6ms 的渲染预算,导致肉眼可见的卡死。

面对密集触发的事件流,我们并不是每次触发都必须立即执行逻辑。根据实际业务诉求,通常有两种降频策略:

  • 防抖(Debounce)策略:将多次密集的触发合并为最后一次。只要用户还在连续操作,计时器就不断重置,直到用户停顿时间超过预设阈值,才真正执行一次目标函数。
  • 节流(Throttle)策略:限制函数的最高执行频次。在持续高频触发的整个时间区间内,强行按照固定时间间隔(如每 200ms)均匀执行一次,保证过程中的阶段性反馈。
JS函数防抖与节流的具体实现代码
图 1:原始高频触发流、防抖处理与节流处理的执行时机与频率对比

二、函数防抖(Debounce)的具体实现代码

“防抖”一词来源于机械开关的去抖动技术:在电键闭合瞬间,金属簧片会产生微秒级的机械颤动,电路通过延时判断来过滤抖动,待信号稳定后再触发闭合逻辑。在 JavaScript 中,防抖的核心就是借助闭包维护一个定时器引用,在规定时间内一旦收到新事件,立即清除旧定时器并重新计时。

JS函数防抖与节流的具体实现代码
图 2:函数防抖(Debounce)的执行与定时器重置步骤流程

1. 基础版防抖实现

基础版防抖函数需要处理两个关键点:利用闭包保存 timer 变量,以及通过 fn.apply(this, args) 准确保留事件监听器被触发时的 this 上下文与原生的 event 事件参数。

/**
 * 基础版防抖函数
 * @param {Function} fn 需要防抖的目标回调函数
 * @param {number} delay 等待延迟时间(毫秒)
 * @returns {Function} 包装后的防抖函数
 */
function debounceBasic(fn, delay) {
  let timer = null;

  return function (...args) {
    // 如果已有定时器,说明上一次等待未结束,直接清除重置
    if (timer !== null) {
      clearTimeout(timer);
    }

    // 重新启动定时器,延迟指定时间后执行
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

2. 生产级完整版防抖(支持立即执行与手动取消)

在实际工程开发中,基础版往往难以满足复杂业务需求:

  • 立即执行(immediate):例如提交按钮防狂点,用户点击的第 1 次应该立即响应提交,后续狂点期间进行防抖冷冻,直到停顿指定时间后才允许再次点击。
  • 手动取消(cancel):当单页应用发生页面路由跳转或组件卸载时,挂载在闭包里的 setTimeout 若不及时销毁,可能会引发内存泄漏或对已销毁的 DOM 进行操作导致报错。
/**
 * 生产级防抖函数(支持首沿立即执行与手动取消)
 * @param {Function} fn 目标函数
 * @param {number} delay 延迟时间(毫秒)
 * @param {boolean} immediate 是否在首次触发时立即执行
 * @returns {Function} 具有 cancel 方法的防抖包装函数
 */
function debounce(fn, delay, immediate = false) {
  let timer = null;

  function debounced(...args) {
    // 如果已有倒计时,立即清空重置
    if (timer !== null) {
      clearTimeout(timer);
    }

    if (immediate) {
      // 如果未处于倒计时状态,说明是首次或冷却结束后的首次,立刻执行
      const callNow = timer === null;

      // 启动定时器,在 delay 毫秒后解除冷却锁定
      timer = setTimeout(() => {
        timer = null;
      }, delay);

      if (callNow) {
        fn.apply(this, args);
      }
    } else {
      // 默认尾沿延时模式
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  }

  // 挂载取消方法,支持在组件卸载或特定交互中重置状态
  debounced.cancel = function () {
    if (timer !== null) {
      clearTimeout(timer);
      timer = null;
    }
  };

  return debounced;
}

3. 防抖在搜索框联想(Input Suggest)中的调用示例

// 业务场景:实时搜索输入联想
const searchInput = document.querySelector('#keyword-input');

function fetchSuggestions(event) {
  const keyword = event.target.value.trim();
  if (!keyword) return;
  console.log(`向服务端发送查询请求: ${keyword}`);
}

// 绑定 400ms 防抖,只有用户停止输入 400ms 后才发送一次请求
const onSearchInput = debounce(fetchSuggestions, 400);
searchInput.addEventListener('input', onSearchInput);

// 在组件销毁或页面重置时注销
// onSearchInput.cancel();

三、函数节流(Throttle)的具体实现代码

“节流”如同水龙头的滴水阀门:哪怕水管水压再大、开关拧得再急,水滴依然只能保持均匀的频率一滴一滴落下。节流的作用是确保函数在持续高频触发下,按照恒定的时间间隔(Interval)有规律地被调度。

JS函数防抖与节流的具体实现代码
图 3:函数节流(Throttle)的时间戳与定时器双机制对比

1. 时间戳版节流(首沿即时触发)

通过比对当前时间戳 now 与上一次执行时间戳 previous。一旦差值超过预设间隔 wait,就立即执行并将 previous 更新为当前时间。

/**
 * 时间戳版节流
 * 特点:首次触发立即执行;停止触发后,如果最后一次在周期内,则不会执行(无尾沿)
 */
function throttleTimestamp(fn, wait) {
  let previous = 0;

  return function (...args) {
    const now = Date.now();
    if (now - previous >= wait) {
      fn.apply(this, args);
      previous = now;
    }
  };
}

2. 定时器版节流(尾沿延迟触发)

当触发事件时,检查定时器是否存在。如果不存在,则建立一个定时器;定时器到期时执行目标函数并清空定时器引用,以便下一次触发重新挂载。

/**
 * 定时器版节流
 * 特点:首次触发不会立即执行,必须等满 wait 毫秒后才首次执行;停止触发后,最后一次仍能履约执行
 */
function throttleTimer(fn, wait) {
  let timer = null;

  return function (...args) {
    if (timer === null) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, wait);
    }
  };
}

3. 生产级完整版节流(融合双机制,支持首尾控制与取消)

生产环境中,单纯的时间戳法会导致尾沿丢失(比如用户停止拖拽后,最后一次停留坐标丢失),而单纯的定时器法会导致初次响应迟钝。更严谨的实现是将两者结合,利用剩余时间 remaining = wait - (now - previous) 进行精准动态调度,并提供配置项控制是否允许首沿(leading)和尾沿(trailing)执行。

/**
 * 生产级节流函数(支持 leading / trailing 可配置与手动取消)
 * @param {Function} fn 目标函数
 * @param {number} wait 间隔周期时间(毫秒)
 * @param {Object} options 配置参数
 * @param {boolean} options.leading 是否在第一次触发时立即执行(默认 true)
 * @param {boolean} options.trailing 是否在停止触发后补调最后一次(默认 true)
 * @returns {Function}
 */
function throttle(fn, wait, options = {}) {
  let timer = null;
  let previous = 0;
  let lastArgs = null;
  let lastThis = null;

  const leading = options.leading !== false;
  const trailing = options.trailing !== false;

  function throttled(...args) {
    const now = Date.now();

    // 如果首次触发且禁用 leading,则将 previous 初始化为当前时间,阻止首次执行
    if (!previous && !leading) {
      previous = now;
    }

    // 计算距离下一次允许执行的剩余等待时间
    const remaining = wait - (now - previous);

    lastThis = this;
    lastArgs = args;

    // 剩余时间已到,或者系统时间被向回调动(now < previous)
    if (remaining <= 0 || remaining > wait) {
      if (timer !== null) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      fn.apply(lastThis, lastArgs);
      lastThis = null;
      lastArgs = null;
    } else if (!timer && trailing) {
      // 处于冷却窗口期内,且开启了尾沿执行,挂载定时器补足剩余时间
      timer = setTimeout(() => {
        previous = !leading ? 0 : Date.now();
        timer = null;
        fn.apply(lastThis, lastArgs);
        lastThis = null;
        lastArgs = null;
      }, remaining);
    }
  }

  // 手动取消
  throttled.cancel = function () {
    if (timer !== null) {
      clearTimeout(timer);
      timer = null;
    }
    previous = 0;
    lastThis = null;
    lastArgs = null;
  };

  return throttled;
}

4. 节流在页面滚动监听(Window Scroll)中的调用示例

// 业务场景:监听滚动条位置,计算吸顶导航显隐与阅读百分比
function handleScroll() {
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
  const progress = ((scrollTop / scrollHeight) * 100).toFixed(1);
  console.log(`当前阅读进度: ${progress}%`);
}

// 限制每 200ms 最多执行一次,兼顾流畅度与性能开销
const onPageScroll = throttle(handleScroll, 200);
window.addEventListener('scroll', onPageScroll);

// 页面跳转前清理
// onPageScroll.cancel();

四、防抖与节流的核心差异与选型依据

初学者常常混淆防抖与节流。区分两者的关键在于判断:在连续操作的过程中,你是否需要周期性地获知进度反馈?

对比维度 函数防抖 (Debounce) 函数节流 (Throttle)
响应机制 延迟合并,静默后触发 固定频率采样,匀速触发
高频持续操作表现 若操作不停止,永远处于重置中,不执行 即使操作不停止,也会按固定周期定期执行
关注核心 只关心用户的最终稳定结果 关心操作过程中的连续平滑状态
典型场景 输入框搜索、窗口调整结束重绘、防表单重复提交 滚动监听、拖拽位移、射击游戏开火、触底加载
JS函数防抖与节流的具体实现代码
图 4:防抖与节流的业务场景决策路径与代码审查清单

五、工程实践中的注意事项与避坑要点

1. this 上下文与原生 Event 对象的透传

在原生的 DOM 事件回调中,this 默认指向绑定事件的 DOM 元素,而首个参数是 MouseEvent 或 InputEvent 等原生事件对象。手写防抖节流时,必须使用 fn.apply(this, args) 将外层包裹函数的 this 与剩余参数原样传递,避免使用固定作用域或普通函数丢失环境。

2. 现代化前端框架(React / Vue)中的闭包陷阱

在 React 函数组件中,如果直接在函数体内调用 const debouncedFn = debounce(fn, 300),每次组件重新渲染都会生成一个全新的防抖函数实例,导致内部的 timer 无法被跨渲染保留,防抖彻底失效。

正确做法是使用 useCallback 配合 useRef 或在 useEffect 中包裹,并在组件注销时显式调用 .cancel() 释放定时器:

// React 中正确使用防抖并处理清理
useEffect(() => {
  const handler = debounce(fetchData, 300);
  window.addEventListener('resize', handler);
  
  return () => {
    handler.cancel(); // 关键:卸载时注销定时器
    window.removeEventListener('resize', handler);
  };
}, []);
3. requestAnimationFrame(rAF)在动画场景下的替代

如果节流的目标仅仅是为了配合屏幕刷新率(如 60FPS)渲染页面动画或重绘 DOM,此时使用基于微秒时间片的 requestAnimationFrame 往往比 throttle(fn, 16.6) 更加高效。因为 rAF 能精确与浏览器的刷新同步,避免掉帧或不必要的离屏渲染。

总结来说,函数防抖和节流是 JavaScript 高性能开发的基本功。理解它们的核心在于理清“延时静默执行”与“固定节拍降频”两种不同流控哲学的差异。在实际开发中,根据业务对“终态”还是“过程态”的诉求灵活选型,并始终处理好 this 绑定与定时器销毁,即可写出兼具性能与健壮性的高水准代码。

作者最新文章

相关标签:

相关文章
更多