带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 对开始触发并在等待期间忽略其他调用。

相关推荐
IT小哥哥呀3 小时前
论文见解:REACT:在语言模型中协同推理和行动
前端·人工智能·react.js·语言模型
一枚前端小能手3 小时前
🚫 请求取消还在用flag?AbortController让你的异步操作更优雅
前端·javascript
code_YuJun3 小时前
前端脚手架开发流程
前端
golang学习记4 小时前
从0死磕全栈之使用 VS Code 调试 Next.js 应用完整指南
前端
Mintopia4 小时前
🧩 隐私计算技术在 Web AIGC 数据处理中的应用实践
前端·javascript·aigc
尘世中一位迷途小书童4 小时前
代码质量保障:ESLint + Prettier + Stylelint 三剑客完美配置
前端·架构
Mintopia4 小时前
🧭 Next.js 架构与运维:当现代前端拥有了“分布式的灵魂”
前端·javascript·全栈
尘世中一位迷途小书童4 小时前
从零搭建:pnpm + Turborepo 项目架构实战(含完整代码)
前端·架构
JarvanMo4 小时前
Flutter 中的 ClipRRect | 每日 Flutter 组件
前端