带leading和trailing的防抖和节流

约定:

  • leading: true --- 在节流/防抖开始时立即触发一次(开头触发)。
  • trailing: true --- 在等待时间结束后触发一次(尾部触发)。
  • 默认 trailing = trueleading = false(与 lodash 默认一致)。

1) debounce(防抖 --- 支持 leading/trailing)

ini 复制代码
function debounce(fn, wait = 0, options = {}) {
  let timer = null;
  let lastArgs = null;
  let lastThis = null;
  let result;
  const leading = !!options.leading;
  const trailing = options.trailing !== false; // 默认 true

  const invoke = () => {
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
  };

  const startTimer = () => {
    timer = setTimeout(() => {
      timer = null;
      // 如果尾部允许并且有待执行的参数,则执行一次
      if (trailing && lastArgs) {
        invoke();
      } else {
        // 如果没有尾调用,清空缓存参数
        lastArgs = lastThis = null;
      }
    }, wait);
  };

  const debounced = function(...args) {
    lastArgs = args;
    lastThis = this;

    // 若没有定时器,说明是新一段触发
    if (!timer) {
      if (leading) {
        // 立即触发(leading)
        invoke();
      }
      // 无论是否 leading,都要启一个定时器用于阻断下一次 leading(并决定是否执行 trailing)
      startTimer();
    } else {
      // 已有定时器,重置等待时间(以便实现防抖的"延后"语义)
      clearTimeout(timer);
      startTimer();
    }
    return result;
  };

  debounced.cancel = function() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    lastArgs = lastThis = null;
  };

  debounced.flush = function() {
    // 立即触发尾部(如果存在)
    if (timer) {
      clearTimeout(timer);
      timer = null;
      if (lastArgs && trailing) invoke();
    }
    return result;
  };

  return debounced;
}

行为说明(常见组合)

  • leading: false, trailing: true(默认)
    -> 仅在停止触发后执行一次(普通防抖)。
  • leading: true, trailing: false
    -> 立即执行一次,之后在 wait 时间内忽略所有调用(只有开头触发)。
  • leading: true, trailing: true
    -> 开头立即执行一次;如果在 wait 内还发生调用,则在 wait 结束后再执行一次(尾部触发传入的最后一次参数)。

使用示例

javascript 复制代码
const deb = debounce((...args) => console.log('run', ...args), 200, { leading: true, trailing: true });
deb(1); // 立即打印 run 1
deb(2); // 不立即打印,200ms后打印 run 2(尾部)

2) throttle(节流 --- 支持 leading/trailing)

实现思路:维护上次实际触发时间 lastInvokeTime 与一个尾部计时器 timer。使用 Date.now() 计算剩余时间。

ini 复制代码
function throttle(fn, wait = 0, options = {}) {
  let timer = null;
  let lastArgs = null;
  let lastThis = null;
  let lastInvokeTime = 0; // 上次实际执行的时间
  let result;
  const leading = options.leading !== false; // 默认 true
  const trailing = options.trailing !== false; // 默认 true

  const now = () => Date.now();

  const invoke = () => {
    lastInvokeTime = now();
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
  };

  const remaining = () => wait - (now() - lastInvokeTime);

  const throttled = function(...args) {
    lastArgs = args;
    lastThis = this;
    const timeLeft = remaining();

    // 一开始如果 lastInvokeTime == 0 && leading == false,我们需要设定 lastInvokeTime
    if (lastInvokeTime === 0 && !leading) {
      lastInvokeTime = now();
    }

    if (timeLeft <= 0 || timeLeft > wait) {
      // 到达可以立即触发的时刻
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      invoke();
    } else if (!timer && trailing) {
      // 安排一次尾部触发(在剩余时间后)
      timer = setTimeout(() => {
        timer = null;
        invoke();
      }, timeLeft);
    }

    return result;
  };

  throttled.cancel = function() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    lastArgs = lastThis = null;
    lastInvokeTime = 0;
  };

  throttled.flush = function() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
      invoke();
    } else if (lastArgs && (now() - lastInvokeTime >= wait)) {
      // 如果没有 timer,但满足触发条件
      invoke();
    }
    return result;
  };

  return throttled;
}

行为说明(常见组合)

  • leading: true, trailing: false(常见)
    -> 第一次调用立即执行,然后在 wait 时间段内忽略后续调用。
  • leading: false, trailing: true
    -> 在首次触发后延迟 wait 时间执行(第一次不是立即触发),之后若再触发,会再次在时间窗结束时触发。
  • leading: true, trailing: true
    -> 开头立即执行;在时间窗内若调用发生,保证在时间窗结束时执行一次(尾部)以处理最后一次调用。

使用示例

javascript 复制代码
const thr = throttle((...args) => console.log('throttle', ...args), 1000, { leading: true, trailing: true });
thr(1); // 立即打印 1
thr(2); // 忽略立即打印,但在 1s 后会打印 2(如果在 wait 期间有调用)

额外说明、注意点与面试要点

  1. this 与 参数 :实现里用 lastThis = thislastArgs = args 存储上下文与参数,确保最终执行时 fnthis 指向正确,且使用最后一次调用的参数(常见语义)。

  2. leading + trailing 组合

    • 如果 leadingtrue,第一次调用会立即触发(如果没有 timer)。
    • 为了避免在 wait 内再次立即触发,我们通常在第一次调用后设置一个阻断 timer。
    • 如果同时 trailingtrue,还需要在 timer 到期时根据是否有"最后一次调用参数"来决定是否再触发一次。
  3. cancel / flush

    • cancel() 用于清除等待与参数(中断),常用于组件卸载时清理。
    • flush() 用于立刻触发尾部(若存在),常用于需要把延后任务立即执行的场景。
  4. 实现上的小坑

    • 不能在 leading 执行后立即把 lastArgs 清空------要保留 lastArgs 以便尾部触发时使用(若 trailing 为 true)。
    • 时间计数需要考虑 Date.now 的稳定性(在高精度场景可用 performance.now())。
    • JS 的 setTimeout 精度与事件循环会有延迟,不保证精确到毫秒。
  5. 测试建议 :写几个场景用例手动测试四种组合(leading/trailing 的 true/false),并测试 cancelflush 行为。

    • 例如:leading:false,trailing:true(普通防抖)对快速连续调用只在最后一次触发;
    • leading:true,trailing:false 对开始触发并在等待期间忽略其他调用。

相关推荐
用泥种荷花16 分钟前
Python环境安装
前端
Light6027 分钟前
性能提升 60%:前端性能优化终极指南
前端·性能优化·图片压缩·渲染优化·按需拆包·边缘缓存·ai 自动化
Jimmy30 分钟前
年终总结 - 2025 故事集
前端·后端·程序员
烛阴32 分钟前
C# 正则表达式(2):Regex 基础语法与常用 API 全解析
前端·正则表达式·c#
roman_日积跬步-终至千里39 分钟前
【人工智能导论】02-搜索-高级搜索策略探索篇:从约束满足到博弈搜索
java·前端·人工智能
GIS之路1 小时前
GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据
前端
多看书少吃饭1 小时前
从Vue到Nuxt.js
前端·javascript·vue.js
前端一小卒1 小时前
从 v5 到 v6:这次 Ant Design 升级真的香
前端·javascript
前端不太难2 小时前
《Vue 项目路由 + Layout 的最佳实践》
前端·javascript·vue.js
LYFlied2 小时前
【每日算法】 LeetCode 56. 合并区间
前端·算法·leetcode·面试·职场和发展