节流

基础知识

  1. 什么是节流 (Throttle)? 节流是另一种与防抖(debounce)齐名的前端性能优化策略。它的核心思想是:确保一个函数在指定的时间间隔内,最多只被执行一次。 无论事件被触发得多频繁,函数都将以固定的频率执行。

  2. 与防抖(Debounce)的关键区别 这是面试和实际应用中必须清晰区分的一点:

    • 防抖 (Debounce): 如果事件被持续触发,动作会被无限期推迟,直到事件停止触发后的一段时间才执行一次。关注的是“空闲”的开始
    • 节流 (Throttle): 如果事件被持续触发,动作会以固定的时间间隔(如每秒一次)被重复执行。关注的是“固定频率”的执行
  3. 生活中的类比 想象一下玩游戏时释放一个有“冷却时间”(Cooldown)的技能:

    • 你按下了技能键,技能被释放。
    • 技能进入 5 秒的冷却期。
    • 在这 5 秒内,无论你多快、多少次地按技能键,技能都不会被再次释放。
    • 只有当 5 秒冷却期结束后,你下一次按键才能再次释放技能。 在这个例子中,技能的释放就被“节流”了。
  4. 应用场景 节流非常适用于那些需要对持续的用户操作给出即时反馈,但又不必对每一次操作都做出响应的场景:

    • 页面滚动事件 (scroll 事件): 在实现无限滚动加载或滚动位置动画时,我们不需要在滚动的每一像素上都进行计算,而是可以每隔 200 毫秒检查一次滚动位置。
    • 拖拽功能 (mousemove 事件): 在实现元素拖拽时,每隔一小段时间更新一次元素位置就足够了,无需在鼠标移动的每个像素点上都重新渲染。
    • 射击游戏中的子弹发射速率

核心思路

节流的实现主要有两种经典的方式:时间戳(Timestamp)定时器(Timer)

  1. 时间戳实现 (Leading Edge):

    • 使用一个变量 previous 记录上一次函数执行的时间戳(初始为 0)。
    • 每次函数被触发时,获取当前时间戳 now
    • 比较 now - previous 是否大于或等于设定的延迟时间 delay
    • 如果是,则说明距离上次执行已经过了足够长的时间,可以执行函数了。执行后,将 previous 更新为 now
    • 如果否,则忽略本次触发。
    • 特点: 这种方式会在时间段的开始立即执行第一次,我们称之为“头部节流”或“前沿节流”。
  2. 定时器实现 (Trailing Edge):

    • 使用一个变量 timer 作为“锁”。初始为 null
    • 每次函数被触发时,检查 timer 是否有值。
    • 如果 timernull(即“锁”是开着的),则设置一个 setTimeout,并将其 ID 赋值给 timer
    • setTimeout 的回调函数在 delay 毫秒后执行原始函数,并且执行完毕后必须将 timer 重置回 null,相当于“开锁”,以便下一次调用可以设置新的定时器。
    • 如果 timer 不为 null,说明“锁”是关着的(上一次的定时器还没执行完),则忽略本次触发。
    • 特点: 这种方式会在时间段的结尾执行,我们称之为“尾部节流”或“后缘节流”。

代码实现

下面分别展示两种方式的实现。

版本一:时间戳实现

/**
 * 节流函数(时间戳版)
 * @param {Function} func - 需要节流的函数
 * @param {number} delay - 时间间隔,单位毫秒
 * @returns {Function} - 返回一个新的节流函数
 */
function throttle_timestamp(func, delay) {
  let previous = 0; // 利用闭包保存上一次执行的时间戳

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

    // 如果当前时间与上一次执行时间之差大于等于延迟时间
    if (now - previous >= delay) {
      func.apply(context, args);
      previous = now; // 更新上一次执行的时间戳
    }
  };
}

版本二:定时器实现

/**
 * 节流函数(定时器版)
 * @param {Function} func - 需要节流的函数
 * @param {number} delay - 时间间隔,单位毫秒
 * @returns {Function} - 返回一个新的节流函数
 */
function throttle_timer(func, delay) {
  let timer = null; // 利用闭包保存定时器 ID

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

    // 如果定时器不存在(即锁是开的)
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(context, args);
        // 执行完毕后,清空定时器,释放锁
        timer = null;
      }, delay);
    }
  };
}

代码解析

时间戳版解析

  1. previous 变量初始化

    • let previous = 0 通过闭包持久化,初始化为 0 确保首次调用时 now - previous >= delay 条件成立。
    • 首次触发事件时,立即执行函数,满足需要即时响应的场景(如拖拽开始)。
  2. 执行条件判断

    • if (now - previous >= delay) 检查距离上次执行是否超过设定延迟。
    • 若超过则执行函数,并更新 previous = now,重置冷却时间。
  3. 冷却期机制

    • 更新 previous 后,在接下来的 delay 毫秒内,now - previous < delay,函数不会再次执行。
    • 实现“前沿节流”,保证在固定周期内只执行一次,适合需要即时反馈但避免频繁执行的场景。

定时器版解析

  1. timer 变量锁机制

    • let timer = null 作为“锁”,timer !== null 表示处于冷却期,新调用被忽略。
    • 初始状态为可执行(锁打开),允许设置定时器。
  2. 定时器设置与加锁

    • if (!timer) 判断锁状态,允许时设置定时器并将 timer 赋值(加锁)。
    • 定时器回调中执行函数,并在完成后重置 timer = null(开锁),释放冷却期。
  3. 后缘执行特性

    • 每次触发事件都会尝试设置定时器,但仅在冷却期结束后执行。
    • 保证持续触发的事件序列中,最后一次事件结束后仍会执行回调(处理最终状态),适合如滚动加载、搜索联想等场景。

两种方式对比

特性 时间戳版(前沿节流) 定时器版(后缘节流)
触发时机 立即触发(首次调用无延迟) 延迟触发(首次调用需等待 delay)
最终事件处理 最后一次触发若未达周期则不执行 最后一次触发后会执行回调处理最终状态
适用场景 拖拽操作、按钮点击防抖动 滚动加载、窗口大小调整
执行模式 固定周期开始时执行 固定周期结束时执行
响应特性 即时响应但可能忽略尾部事件 延迟响应但保证处理最终状态
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""