← 中文资源

简体中文版本

支持首尾触发的节流函数

按固定时间窗限制函数执行频率,可配置首次与末次触发,并提供 cancel 与 flush。

阅读英文原文

功能说明

该示例实现节流函数:在固定时间窗内最多执行一次目标函数。leading 控制第一次调用是否立即执行,trailing 控制窗口结束时是否用最后一次参数补一次调用。它适合滚动监听、拖拽反馈、搜索联想等高频事件,避免回调堆积。

关键代码

throttled 记录 started、lastInvoke 与待处理参数。首次调用根据 leading 决定立即执行还是排入尾部定时器。之后计算距上次执行的时间差,超过 wait 就取消旧定时器并立即执行;否则在 trailing 开启时保存最新参数并调度剩余时间的定时器。invoke 统一清空待处理参数并更新时间戳。cancel 清理定时器和状态,flush 立即执行挂起的尾调用。

使用注意

尾调用只使用最后一次参数,中间参数会被丢弃。函数通过 apply 保留调用时的 this,但箭头函数本身没有自己的 this。wait 为 0 时每次调用都会立即执行。它不是分布式限流器,多实例部署仍需 Redis 或网关限流。组件卸载时应调用 cancel,避免定时器在销毁后继续执行。异步函数被节流时,返回值为 undefined,调用方拿不到原始 Promise。

验证方式

用假定时器连续调用三次,断言 leading 只执行一次,推进时间后 trailing 用最后参数执行一次。测试 leading 为 false 时首次不执行、窗口结束才执行。调用 cancel 后推进时间断言不再执行,调用 flush 断言挂起调用立即执行。最后验证非法 fn、负数 wait 与 this 绑定。

代码示例

export function throttle(fn, wait, options = {}) {
  if (typeof fn !== "function") throw new TypeError("fn must be a function");
  if (!Number.isFinite(wait) || wait < 0) throw new RangeError("wait must be a non-negative number");
  const { leading = true, trailing = true } = options;
  let timer = null;
  let lastInvoke = 0;
  let pendingArgs = null;
  let pendingThis = null;
  let started = false;

  function invoke() {
    const args = pendingArgs;
    const context = pendingThis;
    pendingArgs = null;
    pendingThis = null;
    if (args === null) return;
    lastInvoke = Date.now();
    fn.apply(context, args);
  }

  function schedule(delay) {
    if (timer !== null) return;
    timer = setTimeout(() => {
      timer = null;
      if (trailing) invoke();
    }, delay);
  }

  function throttled(...args) {
    const now = Date.now();
    if (!started) {
      started = true;
      lastInvoke = now;
      if (leading) {
        fn.apply(this, args);
      } else {
        pendingArgs = args;
        pendingThis = this;
        schedule(wait);
      }
      return;
    }
    const elapsed = now - lastInvoke;
    if (elapsed >= wait) {
      if (timer !== null) {
        clearTimeout(timer);
        timer = null;
      }
      lastInvoke = now;
      pendingArgs = null;
      pendingThis = null;
      fn.apply(this, args);
      return;
    }
    if (trailing) {
      pendingArgs = args;
      pendingThis = this;
      schedule(wait - elapsed);
    }
  }

  throttled.cancel = () => {
    if (timer !== null) clearTimeout(timer);
    timer = null;
    pendingArgs = null;
    pendingThis = null;
    started = false;
    lastInvoke = 0;
  };

  throttled.flush = () => {
    if (timer !== null) {
      clearTimeout(timer);
      timer = null;
    }
    invoke();
  };

  return throttled;
}

const send = throttle((value) => console.log("sent", value), 200, {
  leading: true,
  trailing: true,
});
send(1);
send(2);
setTimeout(() => send(3), 250);