防抖与节流:差别只在「谁决定何时执行」

两者都是限流,区别是防抖由最后一次调用决定执行时刻,节流由固定时间窗决定。选错的典型症状是拖拽跟手但松手才更新,或者输入框该等却每 200ms 就发一次。

防抖和节流常被当成「两个相似的限流函数」来记,其实决定它们行为的是同一个问题:执行时刻由谁决定。

  • 防抖(debounce):由最后一次调用决定。每次新调用都重置计时器,只有安静够久才真正执行。
  • 节流(throttle):由固定的时间窗决定。一个窗口内最多执行一次。

防抖:等安静下来

function debounce<T extends unknown[]>(fn: (...a: T) => void, wait: number) {
  let timer: ReturnType<typeof setTimeout> | undefined;
  return (...args: T) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

搜索框输入用它:用户还在打字时不该发请求,停下来了才发。

注意它在输入过程中一次都不执行。如果用户连续打字 10 秒,这 10 秒内触发次数是零 —— 这正是要的,但也意味着「边打边更新字数」不能用它。

节流:按固定节奏执行

function throttle<T extends unknown[]>(fn: (...a: T) => void, interval: number) {
  let last = 0;
  let timer: ReturnType<typeof setTimeout> | undefined;
  return (...args: T) => {
    const now = Date.now();
    const remaining = interval - (now - last);
    if (remaining <= 0) {
      clearTimeout(timer);
      last = now;
      fn(...args);
    } else if (!timer) {
      timer = setTimeout(() => {
        last = Date.now();
        timer = undefined;
        fn(...args);
      }, remaining);
    }
  };
}

滚动进度、拖拽跟随用它:要持续反馈,但不必处理每一个事件。

多出来的 else if (!timer) 分支是关键:只写 if (remaining <= 0) 的话,最后一次事件会被丢掉,拖拽松手后位置停在半路。

怎么选

需求 选择 理由
搜索联想 防抖 输入过程中不该发请求
窗口 resize 重排 防抖 停止拖拽后重排一次就够
滚动进度条 节流 需要持续反馈
拖拽跟随 节流 松手前每一刻都要跟
自动保存 防抖 停止编辑才存

判断方法只有一句:问**「中间的每一次调用,用户能不能感知到」**。感知不到就可以丢,用防抖;感知得到就必须执行,用节流。

两个常见误解

防抖一定比节流省。 不一定。持续输入时防抖确实一次都不跑,更省;但节流保证了稳定的执行频率,对手势中途要刷新状态的场景反而更合适。

requestAnimationFrame 可以替代节流。 rAF 只在页面可见时触发,且频率跟随刷新率(60Hz 与 120Hz 不同)。做纯视觉的滚动跟随它更合适,因为它与绘制同步、不会撕裂;但它不提供「至少每 N 毫秒执行一次」这个保证。

防抖管的是安静,节流管的是节奏。先想清楚要哪一个,两个函数只差三行。

← 返回文章列表

评论

…