核心定义

  • 防抖 (Debounce) 无论触发多少次,只执行最后一次。本质是**“归零重置”**。
  • 节流 (Throttle) 无论触发多少次,按固定频率执行。本质是**“稀释冷却”**。

简单检验 (比喻)

防抖(电梯关门): 哪怕电梯门正在关,只要又进来一个人(新事件触发),电梯门就得重新打开,重新倒计时。直到再也没有人进来了,门才会最终关上。

节流(地铁发车): 即使早高峰站台上挤满了人(高频事件),地铁也只按时刻表(如每5分钟)发一班车。不管这5分钟内来了多少人,车子只按这个节奏走。

机制拆解

A1. 要素 (Components)

  1. 高频触发源:产生密集信号的源头(如鼠标移动、滚动条滚动、键盘输入)。
  2. 定时器 (Timer):控制执行时机的核心守门员。
  3. 执行函数 (Handler):真正需要运行的昂贵逻辑(如API请求、DOM重绘)。
  4. 状态标识 (Flag/Timestamp):记录上一次执行时间或当前是否有待执行任务。

A2. 结构 (Structure)

  • 防抖结构:

    • 事件触发 销毁上一个定时器 建立新定时器 等待 执行。
    • 规则: 只要在等待期间有新事件,之前的努力全部作废。
  • 节流结构:

    • 事件触发 检查当前时间是否超过(上一次执行时间 + 间隔)
      • :执行函数,更新“上一次执行时间”。
      • :无视该事件(或仅更新参数但不执行)。
    • 规则: 只有时间到了通行证才生效,期间的触发被“丢弃”或“合并”。

A3. 系统 (System Function)

当这些零件组合后,该系统实现了**“性能守恒”**:在保证用户体验(反馈)可接受的前提下,将高频的硬件/用户输入信号,转化为低频的程序计算信号,防止CPU过载或API接口被刷爆。

防抖使用情景: 搜索:在用户在输入框输入时,不会立马向后端发起API请求,而是等1000ms后在发起,如果在1000ms内又输入,重置计时器,直到用户停顿超过1000ms后再发起请求。适用于搜索建议、表单验证等场景。

节流使用场景: 快速点赞:当用户快速点击的时候,不会点击一下而直接发起请求,而是等待例如2000ms过后,在统一发起请求记录用户点击的次数,最后清除直接的计时器,在新建一个计时器重新开始计时。适用于滚动加载、按钮防重复提交等

示例代码

/**
 * 防抖函数 (Debounce)
 * @param {Function} func - 要执行的函数
 * @param {number} delay - 延迟时间 (毫秒)
 * @returns {Function} 包装后的防抖函数
 */
function debounce(func, delay) {
    let timeoutId = null; // 用于存储计时器 ID
 
    // 返回一个新的函数,这个函数才是事件实际绑定的处理函数
    return function(...args) {
        // 保存函数的执行上下文 (this) 和参数
        const context = this;
 
        // 每次触发事件时,都清除上一个计时器
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
 
        // 重新设置一个新的计时器
        timeoutId = setTimeout(() => {
            // 当计时器时间到了,执行真正的函数
            func.apply(context, args);
            timeoutId = null; // 执行后清空 ID
        }, delay);
    };
}
 
// --- 示例应用 ---
 
const handleInput = (text) => {
    console.log(`发送搜索请求: ${text} (${new Date().toLocaleTimeString()})`);
};
 
// 创建一个防抖处理函数,延迟 500 毫秒
const debouncedHandleInput = debounce(handleInput, 500);
 
// 模拟用户快速输入
console.log("--- 防抖开始 ---");
debouncedHandleInput("a"); // 触发 1
debouncedHandleInput("ab"); // 触发 2 (取消 1)
debouncedHandleInput("abc"); // 触发 3 (取消 2)
 
// 假设用户停止输入 500ms 后... 只有 'abc' 的请求会执行
setTimeout(() => {
    console.log("--- 停止触发,等待 500ms 后执行 ---");
}, 700);

节流:

/**
 * 节流函数 (Throttle) - 时间戳实现
 * @param {Function} func - 要执行的函数
 * @param {number} delay - 限制的间隔时间 (毫秒)
 * @returns {Function} 包装后的节流函数
 */
function throttle(func, delay) {
    let lastTime = 0; // 上次执行的时间
 
    return function(...args) {
        const context = this;
        const now = Date.now(); // 当前时间
 
        // 如果距离上次执行的时间间隔大于或等于设定的延迟时间
        if (now - lastTime >= delay) {
            // 执行真正的函数
            func.apply(context, args);
            // 更新上次执行的时间
            lastTime = now;
        }
        // 否则,忽略本次调用
    };
}
 
// --- 示例应用 ---
 
const handleScroll = () => {
    console.log(`处理滚动事件... (${new Date().toLocaleTimeString()})`);
};
 
// 创建一个节流处理函数,限制每 1000 毫秒(1秒)最多执行一次
const throttledHandleScroll = throttle(handleScroll, 1000);
 
// 模拟用户在 1500 毫秒内连续触发
console.log("--- 节流开始 ---");
throttledHandleScroll(); // 0ms: 立即执行 (lastTime = 0)
throttledHandleScroll(); // 100ms: 忽略 (时间未到 1000ms)
throttledHandleScroll(); // 500ms: 忽略
throttledHandleScroll(); // 999ms: 忽略
// 假设用户在 1001ms 时再次触发
setTimeout(() => {
    throttledHandleScroll(); // 1001ms: 执行 (因为 now - lastTime > 1000)
    throttledHandleScroll(); // 1100ms: 忽略
}, 1001);