彻底搞懂节流 (Throttling) 与防抖 (Debouncing) - 原理、场景与实战代码

在开发中,我们经常会遇到一些高频触发的事件,比如窗口的 resizescroll,鼠标的 mousemovemouseover,或者用户在输入框中连续输入。如果不对这些事件的回调函数进行处理,可能会导致函数被频繁执行,极大地消耗浏览器性能,甚至引发页面卡顿、假死,同时也可能给服务器带来不必要的压力。

这时,节流 (Throttling)防抖 (Debouncing) 就派上用场了。它们的核心思想都是通过限制实际执行函数的频率来提高性能和用户体验。虽然目标相似,但它们的实现方式和适用场景却有所不同。

今天,我们就来彻底搞懂这两个概念!


一、 问题来了:为什么需要节流和防抖?

在日常的前端开发中,有许多用户交互会高频率地触发JavaScript事件。例如,当用户调整浏览器窗口大小 (resize)、滚动页面 (scroll),或者在输入框中快速输入文字 (input/keyup) 时,相关的事件监听器会被以极高的频率调用。

那么,如果不对这些高频触发的事件回调进行控制,会发生什么呢?

简单来说,会引发性能问题、资源浪费和糟糕的用户体验。 这就是我们为什么需要节流 (Throttling) 和防抖 (Debouncing) 的核心原因。它们是用来优化高频事件处理,确保我们的应用流畅高效的关键技术。

让我们通过一些具体场景来理解这些“后果”:

  1. 搜索框实时搜索: 用户每输入一个字符,就向服务器发送一次请求获取搜索建议。如果用户快速输入 "JavaScript",可能会瞬间发送10次请求!
    • 后果 -> 资源浪费、服务器压力增大、潜在的性能瓶颈。
  2. 页面滚动加载更多/动画效果: 监听 scroll 事件,每次滚动都进行复杂的计算(如判断是否到达底部、更新元素位置和状态)。如果滚动非常快,这些计算也会执行得非常频繁。
    • 后果 -> 性能问题(大量计算导致CPU占用过高,页面卡顿、掉帧)。
  3. 窗口大小调整: 监听 resize 事件,每次窗口大小的微小变化都可能触发复杂的页面元素布局重计算。
    • 后果 -> 性能问题(频繁的重排重绘,导致页面响应迟钝)。

总结一下,这些场景下,回调函数被密集调用的主要后果是:

  • 性能问题: 大量不必要的计算和DOM操作,尤其是在短时间内密集发生,会导致CPU占用率飙升,主线程阻塞,从而引发页面卡顿、动画掉帧,甚至浏览器假死。
  • 资源浪费: 以搜索框为例,如果用户目标是输入 "JavaScript",那么在输入过程中发送的 "J", "Ja", "Jav"... 等中间状态的API请求,大部分是无效的,这不仅浪费了用户的网络带宽,也给服务器带来了不必要的处理压力。
  • 糟糕的用户体验: 页面响应迟钝、操作不跟手、动画卡顿,这些都会严重影响用户对应用的整体感受。

来看一个直观的例子:

如果我们直接监听输入框的 input 事件,不加任何处理:


API请求次数: 0

当你尝试在这个输入框中快速打字时,你会立刻在控制台看到 callApi 函数被疯狂调用,API请求次数也随之急剧增加。这清晰地暴露了未经优化的事件处理所带来的问题。

因此,为了解决上述由高频事件触发带来的性能瓶颈、资源浪费和用户体验下降的问题,我们就需要引入节流和防抖机制来智能地控制事件回调的执行频率。


二、 防抖 (Debouncing)

概念:

防抖的核心思想是:当一个事件持续触发时,相应的回调函数并不会立即执行,而是会等待一段时间(例如300毫秒)。如果在这段时间内,事件没有再次被触发,那么回调函数才会执行。如果在这段时间内事件又被触发了,那么就重新开始计时。

通俗理解:

  • 电梯关门: 想象你在电梯里,有人不停地按开门按钮,电梯门就不会关。只有当一段时间内没人再按开门按钮了,电梯门才会关上。
  • 王者荣耀回城: 你点击回城,如果期间受到攻击,回城就会被打断,需要重新等待回城引导时间。

如何工作(核心步骤):

  1. 当事件首次触发时,设置一个定时器,在指定延迟时间后执行回调函数。
  2. 如果在这个延迟时间内,事件再次被触发,则清除之前的定时器,并重新设置一个新的定时器。
  3. 只有当延迟时间内没有新的事件触发,定时器到期,回调函数才会真正执行。

JavaScript 实现:

下面是一个简单的 debounce 函数实现:

function debounce(func, delay) {
  let timerId; // 用于存储定时器ID

  // 返回一个新的函数
  return function(...args) {
    // 保存 this 上下文和参数
    const context = this;

    // 如果已经存在定时器,则清除它
    if (timerId) {
      clearTimeout(timerId);
    }

    // 设置新的定时器
    timerId = setTimeout(() => {
      // 当定时器触发时,调用原始函数,并传入正确的this和参数
      func.apply(context, args);
      timerId = null; // 执行完毕后,重置timerId
    }, delay);
  };
}

使用示例(改造上面的搜索框):


API请求次数 (防抖): 0

现在,当你快速输入时,API请求只会在你停止输入500毫秒后才会触发一次。

防抖的应用场景:

  • 搜索框输入建议: 用户输入完成后(停顿一段时间)再发送请求。
  • 输入内容验证: 用户停止输入后进行格式校验。
  • 窗口 resize 事件: 用户停止调整窗口大小后,再重新计算布局。
  • 表单自动保存: 用户停止编辑一段时间后自动保存草稿。
  • 按钮重复点击: 防止用户快速点击按钮导致多次提交(虽然节流有时也适用,但防抖更侧重于最终状态)。

防抖的变种:立即执行

有时候我们希望事件第一次触发时立即执行回调,然后在停止触发一段时间后才能再次触发(冷却期)。

function debounceLeading(func, delay, immediate = false) {
  let timerId;
  let lastCallTime = 0; // 记录上一次调用的时间戳(对于非立即执行)或是否已执行(对于立即执行)

  return function(...args) {
    const context = this;
    const now = Date.now();

    const later = () => {
      timerId = null;
      if (!immediate) { // 如果不是立即执行,则在延迟后执行
        func.apply(context, args);
      }
    };

    const callNow = immediate && !timerId; // 立即执行的条件:immediate为true且没有正在进行的timer

    clearTimeout(timerId); // 清除之前的定时器
    timerId = setTimeout(later, delay);

    if (callNow) {
      func.apply(context, args);
    }
  };
}

// 示例:点击按钮立即执行一次,之后500ms内再点击无效,500ms后可再次立即执行
const immediateDebouncedClick = debounceLeading(() => {
  console.log('Button clicked! (Immediate Debounce)');
}, 500, true);

// document.getElementById('myButton').addEventListener('click', immediateDebouncedClick);

注:上面这个 debounceLeading 的实现,更像是“首次立即执行,后续触发会重置延迟,延迟结束后才能再次立即执行”的模式。对于标准的 “leading debounce”,通常是第一次触发立即执行,然后进入一个冷却期,冷却期内再触发会重新计时(但不会执行),直到冷却期结束后再触发才能再次立即执行。更简单的 immediate 实现是在 debounce 内部判断是否是首次调用且 immediatetrue

一个更常见的 immediate 防抖实现思路是:

function debounce(func, delay, immediate = false) {
  let timerId;

  return function(...args) {
    const context = this;
    const callNow = immediate && !timerId; // 是否立即执行

    clearTimeout(timerId); // 总是清除之前的定时器

    timerId = setTimeout(() => {
      timerId = null; // 允许下一次事件的 timer 被设置
      if (!immediate) { // 如果不是立即执行,则在延迟结束后执行
        func.apply(context, args);
      }
    }, delay);

    if (callNow) { // 如果是立即执行,并且 timerId 为空(即非冷却期)
      func.apply(context, args);
    }
  };
}

这个函数中,如果 immediatetrue,第一次事件会立即执行 func。后续事件在 delay 时间内会不断重置定时器,但因为 timerId 不为 null,所以 callNowfalse,不会立即执行。直到 delay 时间过后,timerIdsetTimeout 回调中被设为 null,下一次事件才能再次满足 callNow 的条件。


三、 节流 (Throttling):

概念:

节流的核心思想是:在一个固定的时间间隔内,无论事件被触发多少次,回调函数都只会被执行一次。

通俗理解:

  • 技能冷却: 游戏中的技能,使用后会进入冷却时间,冷却时间内无法再次使用,必须等到冷却结束。
  • 水龙头: 水龙头开到最大,单位时间内流出的水量也是固定的,不会因为你拧得更用力而瞬间喷出更多水。

如何工作(核心步骤 - 基于时间戳或定时器):

  1. 基于时间戳:
    • 记录上一次函数执行的时间戳。
    • 当事件触发时,获取当前时间戳。
    • 如果当前时间戳与上一次执行时间戳的差值大于或等于设定的延迟时间,则执行函数,并更新上一次执行的时间戳。
    • 否则,不执行。
  2. 基于定时器:
    • 当事件触发时,如果当前没有正在运行的定时器,则设置一个定时器,在指定延迟时间后执行回调函数,并在定时器触发后清除定时器标记。
    • 如果在定时器运行期间事件再次触发,则不执行任何操作(因为定时器仍在)。

JavaScript 实现:

1. 基于时间戳的节流(通常首次会立即执行,停止触发后不会再执行):

function throttleTimestamp(func, delay) {
  let lastExecutionTime = 0; // 上一次执行的时间戳

  return function(...args) {
    const context = this;
    const currentTime = Date.now();

    if (currentTime - lastExecutionTime >= delay) {
      func.apply(context, args);
      lastExecutionTime = currentTime; // 更新执行时间
    }
  };
}

2. 基于定时器的节流(首次不会立即执行,停止触发后,如果最后一次触发在延迟内,仍会执行一次):

function throttleTimeout(func, delay) {
  let timerId = null; // 定时器ID

  return function(...args) {
    const context = this;

    if (!timerId) { // 如果没有定时器在运行
      timerId = setTimeout(() => {
        func.apply(context, args);
        timerId = null; // 执行完毕,清除定时器ID,允许下一次设置
      }, delay);
    }
  };
}

使用示例(监听 scroll 事件):

滚动我!

节流执行次数: 0

当你快速滚动时,你会发现 handleScroll 函数大约每1秒才执行一次。

节流的应用场景:

  • 页面滚动事件 (scroll): 如实现无限滚动加载、滚动到指定位置触发动画、更新导航栏状态等,但不需要每次滚动像素都触发。
  • 鼠标移动事件 (mousemove): 如实现拖拽功能、绘制图形等,按一定频率更新位置。
  • DOM 元素拖拽: 限制拖拽过程中位置更新的频率。
  • 游戏中的射击/技能释放: 控制射击频率或技能冷却。
  • 高频点击: 如点赞按钮,限制单位时间内的点击次数(防抖也可以,但节流保证规律性)。

节流的变种:控制首次执行和末次执行

更完善的节流函数通常会提供选项来控制是否在第一次触发时立即执行(leading edge)以及是否在最后一次触发后(如果在时间间隔内)再执行一次(trailing edge)。

function throttle(func, delay, options = { leading: true, trailing: true }) {
  let timerId = null;
  let lastArgs = null;
  let lastThis = null;
  let lastCallTime = 0;

  const invokeFunc = (time) => {
    if (lastArgs) {
      func.apply(lastThis, lastArgs);
      lastCallTime = time;
      lastArgs = lastThis = null; // 清除,防止重复执行
    }
  };

  const throttled = function(...args) {
    const now = Date.now();
    if (!lastCallTime && !options.leading) { // 如果是第一次调用且leading为false,则记录时间但不立即执行
      lastCallTime = now;
    }

    const remaining = delay - (now - lastCallTime);
    lastArgs = args;
    lastThis = this;

    if (remaining <= 0 || remaining > delay) { // 时间已到或系统时间被调整
      if (timerId) {
        clearTimeout(timerId);
        timerId = null;
      }
      lastCallTime = now;
      invokeFunc(now);
    } else if (!timerId && options.trailing) { // 如果没有定时器,并且需要末尾执行
      timerId = setTimeout(() => {
        lastCallTime = options.leading ? 0 : Date.now(); // 如果leading为false,下次可以立即执行
        timerId = null;
        invokeFunc(lastCallTime);
      }, remaining);
    }
  };

  throttled.cancel = () => {
    clearTimeout(timerId);
    lastCallTime = 0;
    timerId = lastArgs = lastThis = null;
  };

  return throttled;
}

// 使用示例
// const advancedThrottledScroll = throttle(() => console.log('Advanced Throttled Scroll!'), 1000, { leading: true, trailing: true });
// window.addEventListener('scroll', advancedThrottledScroll);
// setTimeout(() => advancedThrottledScroll.cancel(), 5000); // 5秒后取消

四、 防抖 (Debounce) vs 节流 (Throttle):到底用哪个?

特性 防抖 (Debounce) 节流 (Throttle)
核心思想 事件停止触发一段时间后才执行 固定时间间隔内最多执行一次
关注点 关注“结果”:一系列事件最终完成后执行一次 关注“过程”:一系列事件中规律性地执行
执行次数 如果事件持续触发,可能一次都不执行,或只执行一次 在持续触发过程中,会按设定的频率多次执行
例子 搜索建议(等你打完字) 游戏射击(每秒固定子弹数)
适用场景 输入验证、自动保存、窗口resize后计算布局 滚动加载、拖拽、高频API调用(如位置上报)

简单决策指南:

  • 如果你希望事件在“停止”或“完成后”才触发一次: 使用 防抖 (Debounce)
    • 例如:
      • 1.用户输入搜索词后,停顿了才去请求API。 
      • 2.窗口调整完毕后,才重新计算布局。
  • 如果你希望事件在持续触发过程中,能以一个固定的频率去响应: 使用 节流 (Throttle)
    • 例如:
      • 1.页面滚动时,每隔200毫秒检查一次是否快到底部。
      • 2.鼠标拖拽时,每隔50毫秒更新一次元素位置。

五、 实现与使用时的重要注意事项

仅仅实现一个基础的 debouncethrottle 函数只是第一步。在实际开发中,为了确保这些工具函数健壮、灵活、易用,并且能正确地融入复杂应用场景,我们还需要关注以下几个重要的注意事项:

  1. this 指向和 arguments 的正确处理

    这是确保工具函数能够正确包装原始函数的基础。

    • 核心机制: 在我们之前提供的 debouncethrottle 函数实现中,都使用了 func.apply(context, args)(或者也可以用 func.call(context, ...args))。这样做就是为了:
      • 确保 this 指向正确: 原始函数 func 在执行时,其内部的 this 关键字会指向调用“包装后函数”(即 debouncethrottle 返回的那个函数)时的上下文。
      • 确保参数传递正确: 所有传递给“包装后函数”的参数 (args) 都会被原封不动地传递给原始函数 func
    • 回顾关键代码: 
      // 在 debounce 或 throttle 返回的函数内部
      // return function(...args) { // ...args 用来收集所有参数
      //   const context = this;    // 'this' 是调用包装后函数时的上下文
      //   // ...
      //   timerId = setTimeout(() => {
      //     func.apply(context, args); // 执行原始函数,并指定正确的this和参数
      //   }, delay);
      // };
      
    • 注意: 如果你有一个对象方法需要防抖或节流,并且该方法依赖于 this 指向该对象,你需要确保调用包装函数时 this 的正确性(例如,通过事件监听器调用时,this 可能是DOM元素),或者在传递原始函数时预先绑定 this(例如使用 originalMethod.bind(myObject))。
  2. 提供取消功能 

    在动态应用中,能够主动取消一个待执行的防抖或节流操作非常重要。

    • 原因:
      • 避免错误: 当相关组件即将销毁或条件不再满足时,取消可以防止函数在不合适的时机执行(例如,对一个已不存在的DOM元素进行操作)。
      • 资源管理: 清除不再需要的定时器,有助于避免潜在的内存泄漏。
    • 实现方式: 为包装后的函数添加一个 cancel 方法,该方法内部调用 clearTimeout() 来清除待执行的定时器。
    • debounce 函数添加 cancel 方法示例: 
      function debounceWithCancel(func, delay) {
        let timerId;
      
        const debouncedFunction = function(...args) {
          const context = this;
          clearTimeout(timerId);
          timerId = setTimeout(() => {
            func.apply(context, args);
            timerId = null;
          }, delay);
        };
      
        debouncedFunction.cancel = function() {
          clearTimeout(timerId);
          timerId = null;
          console.log("Debounced execution cancelled.");
        };
      
        return debouncedFunction;
      }
      
      // --- 使用示例 ---
      // const debouncedOperation = debounceWithCancel(someFunction, 1000);
      // debouncedOperation(); // 触发
      // // ... 某个时刻 ...
      // debouncedOperation.cancel(); // 主动取消
      
    • 对于节流函数,如果其实现依赖 setTimeout(例如控制末尾执行的定时器),也应提供类似的 cancel 机制。
  3. 首次/末尾执行控制 

    标准的防抖和节流行为并不总是能满足所有需求。提供选项来控制首次或末尾的执行时机,可以大大增强工具的灵活性。

    • 概念演示: 假设我们有一个更高级的 advancedDebounce 函数,它可以接受一个配置对象。 
      // 假设的 advancedDebounce 函数签名:
      // function advancedDebounce(func, delay, options = { leading: false, trailing: true }) { /* ... */ }
      
      function logClick(eventName) {
        console.log(`${eventName} click logged!`);
      }
      
      // 1. 默认防抖 (末尾执行 - trailing: true, leading: false)
      // const trailingClick = advancedDebounce(logClick, 1000, { leading: false, trailing: true });
      // button1.addEventListener('click', () => trailingClick("Trailing"));
      // 效果:快速点击按钮,只在最后一次点击停止1秒后,logClick 执行一次。
      
      // 2. 首次立即执行防抖 (leading: true, trailing: false)
      // const leadingClick = advancedDebounce(logClick, 1000, { leading: true, trailing: false });
      // button2.addEventListener('click', () => leadingClick("Leading"));
      // 效果:第一次点击按钮立即执行 logClick,之后1秒内的连续点击无效,直到1秒冷却期过后再次点击才会立即执行。
      
      // 3. 首次和末尾都可能执行 (leading: true, trailing: true)
      // const leadingAndTrailingClick = advancedDebounce(logClick, 1000, { leading: true, trailing: true });
      // button3.addEventListener('click', () => leadingAndTrailingClick("Leading & Trailing"));
      // 效果:第一次点击立即执行。如果在1秒冷却期内还有点击,那么在最后一次点击停止1秒后还会再执行一次。
      
    • 对于节流 (Throttle): 同样可以有 leadingtrailing 选项,决定是在时间间隔的开始、结束,还是两者都执行回调。 
      // 假设的 advancedThrottle 函数签名:
      // function advancedThrottle(func, delay, options = { leading: true, trailing: false }) { /* ... */ }
      
      function handleScroll(eventInfo) {
        console.log(`Scroll event throttled: ${eventInfo}`);
      }
      
      // 1. 首次执行节流 (leading: true, trailing: false)
      // const leadingScroll = advancedThrottle(handleScroll, 1000, { leading: true, trailing: false });
      // window.addEventListener('scroll', () => leadingScroll("Leading Edge"));
      // 效果:滚动开始时立即执行一次,之后每隔1秒,如果仍在滚动,会在下一个1秒周期的开始时执行。
      
      // 2. 末尾执行节流 (leading: false, trailing: true)
      // const trailingScroll = advancedThrottle(handleScroll, 1000, { leading: false, trailing: true });
      // window.addEventListener('scroll', () => trailingScroll("Trailing Edge"));
      // 效果:滚动时,不会立即执行。在每个1秒周期的末尾,如果该周期内发生了滚动,则执行一次。
      
    • 重要性: 理解并能配置这些选项,可以让你的防抖/节流更好地适应具体业务场景,例如用户希望按钮点击立即有反馈,或只关心输入完成后的最终状态。
  4. 原始函数的返回值

    由于 debouncethrottle 通常依赖 setTimeout 来延迟执行原始函数,原始函数的执行是异步的。

    • 直接返回值问题: 包装后的防抖/节流函数(例如 debouncedFunc())通常不会立即返回原始函数 func 的执行结果。 
      function calculateSomething(value) {
        console.log(`Calculating with ${value}...`);
        return value * 2;
      }
      
      const debouncedCalculate = debounceWithCancel(calculateSomething, 500); // 使用我们之前的debounce
      
      const result = debouncedCalculate(10);
      console.log("Result from calling debouncedCalculate:", result); // 输出: undefined
      // calculateSomething 会在500ms后执行,但其返回值无法赋给 result
      
      // 500ms 后控制台会输出: "Calculating with 10..."
      
    • 解决方案如何获取“结果”:
      • 副作用(最常见): 原始函数通常通过直接修改外部状态、更新UI或执行其他有副作用的操作来“返回”其工作成果。 
        let processedValue = null;
        function processAndUpdate(value) {
          processedValue = value * 2; // 修改外部状态
          // document.getElementById('resultDisplay').textContent = processedValue; // 更新UI
          console.log(`Processed value is now: ${processedValue}`);
        }
        const debouncedProcess = debounceWithCancel(processAndUpdate, 500);
        debouncedProcess(20); // 500ms后 processedValue 会被更新,UI也会被更新
        
      • 回调函数: 原始函数可以接受一个回调,在完成时调用这个回调并传入结果。
      • Promise: 对于返回 Promise 的原始函数,可以设计更复杂的防抖/节流函数来也返回 Promise(这超出了基础实现的范畴)。
    • 重要性: 开发者需要清楚这一点,避免错误地期望从包装函数中直接获得原始函数的同步返回值,并据此设计数据的流转方式。
  5. 选择合适的延迟时间

    delay 参数是防抖和节流函数的核心,其值的设定直接影响用户体验和性能优化的效果。这更多的是使用层面的经验,而非具体代码实现。

    • 影响演示(概念性):
      // 场景:输入框实时搜索建议
      // function fetchSuggestions(query) { /* API 调用 */ }
      
      // const debouncedSearch1 = debounceWithCancel(fetchSuggestions, 50);
      // console.log("Delay 50ms: 可能仍然过于频繁,用户每输入几个字符就触发一次。");
      
      // const debouncedSearch2 = debounceWithCancel(fetchSuggestions, 300);
      // console.log("Delay 300ms: 较为合理,用户短暂停顿后触发,体验较好。");
      
      // const debouncedSearch3 = debounceWithCancel(fetchSuggestions, 1500);
      // console.log("Delay 1500ms: 可能太长,用户输入完毕后需等待较长时间才有建议,感觉迟钝。");
      
    • 原则: delay 的选择没有固定标准,需要根据具体的交互场景、用户的预期反应速度以及性能瓶颈的实际情况来权衡和测试。
    • 重要性: 这是使用层面最需要仔细调整的参数,直接关系到优化措施是否有效且用户友好。
  6. 定时器管理与组件生命周期 

    在现代前端框架(如React, Vue, Angular等)构建的单页应用(SPA)中,组件会频繁地创建和销毁。妥善管理定时器至关重要。

    • 潜在问题: 如果组件销毁了,但其内部启动的防抖/节流定时器仍在运行,可能导致内存泄漏或试图操作已不存在的组件状态。
    • 解决方案(利用 cancel 方法): 
      // 简化版演示,非特定框架,说明概念
      class MyInteractiveComponent {
        constructor() {
          this.inputValue = "";
          // 对 handleInput 方法进行防抖处理,并保存取消函数
          this.debouncedHandleInput = debounceWithCancel(this.processInput.bind(this), 500);
          console.log("Component created, debounced handler set up.");
        }
      
        processInput() {
          console.log(`Processing input: ${this.inputValue}`);
          // 假设这里进行一些基于 inputValue 的操作
        }
      
        // 模拟用户输入
        simulateInput(newValue) {
          this.inputValue = newValue;
          this.debouncedHandleInput(); // 调用防抖函数
        }
      
        // 模拟组件销毁
        destroy() {
          console.log("Component destroying, cancelling pending debounced input handler.");
          this.debouncedHandleInput.cancel(); // 清除定时器
        }
      }
      
      // const myComponent = new MyInteractiveComponent();
      // myComponent.simulateInput("test1");
      // myComponent.simulateInput("test2"); // "test1"的 processInput 会被取消
      
      // // 假设在 "test2" 的 processInput 执行前组件被销毁
      // setTimeout(() => {
      //   myComponent.destroy();
      // }, 300); // 在500ms延迟之内销毁
      // // 此时,"test2" 的 processInput 也因为 cancel 调用而不会执行。
      
    • 重要性: 在SPA中,这是确保应用长期稳定运行,避免难以追踪的bug和性能下降的关键实践。总是在组件卸载或不再需要时清理定时器。

六、 总结

防抖和节流是前端开发中非常实用的性能优化技巧。

  • 防抖 (Debounce): “等你不动了我再动”,适用于只关心最终结果的场景。
  • 节流 (Throttle): “按节奏来动”,适用于需要在持续事件中规律执行的场景。

你可能感兴趣的:(前端,开发语言,javascript)