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

相关推荐
顾安r1 小时前
11.8 脚本网页 星际逃生
c语言·前端·javascript·flask
Hello.Reader1 小时前
Data Sink定义、参数与可落地示例
java·前端·网络
im_AMBER1 小时前
React 17
前端·javascript·笔记·学习·react.js·前端框架
谷歌开发者2 小时前
Web 开发指向标 | Chrome 开发者工具学习资源 (六)
前端·chrome·学习
一晌小贪欢3 小时前
【Html模板】电商运营可视化大屏模板 Excel存储 + 一键导出(已上线-可预览)
前端·数据分析·html·excel·数据看板·电商大屏·大屏看板
发现你走远了3 小时前
连接模拟器网页进行h5的调试(使用Chrome远程调试(推荐)) 保姆级图文
前端·chrome
街尾杂货店&4 小时前
css - 实现三角形 div 容器,用css画一个三角形(提供示例源码)简单粗暴几行代码搞定!
前端·css
顺凡4 小时前
删一个却少俩:Antd Tag 多节点同时消失的原因
前端·javascript·面试
小白路过4 小时前
CSS transform矩阵变换全面解析
前端·css·矩阵
爬山算法4 小时前
Redis(110)Redis的发布订阅机制如何使用?
前端·redis·bootstrap