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

相关推荐
这是个栗子22 分钟前
npm报错 : 无法加载文件 npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
HIT_Weston1 小时前
44、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 分析(一)
前端·ubuntu·gitlab
华仔啊1 小时前
Vue3 如何实现图片懒加载?其实一个 Intersection Observer 就搞定了
前端·vue.js
JamesGosling6662 小时前
深入理解内容安全策略(CSP):原理、作用与实践指南
前端·浏览器
不要想太多2 小时前
前端进阶系列之《浏览器渲染原理》
前端
g***96902 小时前
Node.js npm 安装过程中 EBUSY 错误的分析与解决方案
前端·npm·node.js
七喜小伙儿2 小时前
第2节:趣谈FreeRTOS--打工人的日常
前端
我叫张小白。2 小时前
Vue3 响应式数据:让数据拥有“生命力“
前端·javascript·vue.js·vue3
laocooon5238578862 小时前
vue3 本文实现了一个Vue3折叠面板组件
开发语言·前端·javascript
IT_陈寒2 小时前
React 18并发渲染实战:5个核心API让你的应用性能飙升50%
前端·人工智能·后端