一句话:节流是「隔一段时间才准执行一次」,防抖是「停下来才执行」——滚动用节流,搜索输入用防抖。

写在前面

高频事件(scroll、resize、input)若每次触发都请求接口或重算布局,会拖垮性能。 节流(throttle) 和 防抖(debounce) 是前端最常用的两种包装策略。二者区别常被记混:核心在于 首次是否立即执行 以及 连续触发时的计时逻辑。

核心内容

节流(throttle)

语义:在固定时间间隔内,无论触发多少次,函数最多执行一次。

典型场景:页面滚动加载、鼠标移动跟踪、按钮防重复点击、拖拽。

时间戳版(首次立即执行,结束边界可能多触发一次):

export function throttle(fn, delay) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

定时器版(首次也受 delay 约束,结束更均匀):

export function throttle(fn, delay = 500) {
  let flag = true
  return function (...args) {
    if (flag) {
      fn.apply(this, args)
      flag = false
      setTimeout(() => { flag = true }, delay)
    }
  }
}

使用示例:

const throttledScroll = throttle(() => {
  console.log('滚动中...')
}, 2000)
window.addEventListener('scroll', throttledScroll)

防抖(debounce)

语义:连续触发时只认「最后一次」;每次触发都重置等待计时,静默 delay 毫秒后才真正执行。

典型场景:搜索框联想、窗口 resize 结束后重绘、表单校验。

export function debounce(fn, delay) {
  let timerId = null
  return function (...args) {
    if (timerId) clearTimeout(timerId)
    timerId = setTimeout(() => {
      fn.apply(this, args)
      timerId = null
    }, delay)
  }
}
const debouncedInput = debounce(() => {
  console.log('输入稳定,发起搜索')
}, 500)
document.getElementById('inputField').addEventListener('input', debouncedInput)

leading 防抖(可选):第一次立即执行,后续连续输入仍合并——lodash 的 debounce(fn, wait, { leading: true, trailing: false }) 即此类。

对比一览

维度 节流 防抖
连续触发 按固定间隔执行 重新计时,静默后才执行
首次触发 时间戳版立即执行 默认延迟后执行
比喻 地铁发车间隔 电梯等人齐再关门
场景 scroll、mousemove input 搜索、resize
sequenceDiagram
  participant E as 事件
  participant T as 节流
  participant D as 防抖
  Note over E,T: 每 200ms 最多 1 次
  E->>T: 触发×10
  T->>T: 执行约 5 次
  Note over E,D: 停止输入 500ms 后 1 次
  E->>D: 触发×10
  D->>D: 最终执行 1 次

Vue / 现代工具库

  • VueUse:useDebounceFn、useThrottleFn
  • lodash:debounce、throttle(注意 throttle 已标记 deprecated,新项目可自实现或换 @vueuse/core)

在线对比 Demo:jiang-xia.top/tool/test

踩坑

  1. 箭头函数丢失 this:包装函数内用 fn.apply(this, args) 保留调用上下文。
  2. 组件卸载未取消:debounce 的 timer 应在 onUnmounted 里 clearTimeout,或取消防抖函数的 pending 调用。
  3. 节流 trailing 丢失最后一次:时间戳版可能在最后一次事件后不再执行;可结合 trailing 标志或使用 lodash 选项。
  4. delay 单位混用:统一毫秒;与 requestAnimationFrame 配合时 scroll 动画更顺滑。
  5. 把 debounce 当节流用:搜索框用 throttle 会导致每 300ms 打一次接口,仍过多。

小结

  • 节流:控频率,滚动/拖拽/按钮连点。
  • 防抖:等稳定,搜索/resize/提交校验。
  • 实现要点:闭包保存 timer / lastTime;记得 apply(this, args) 与卸载清理。

延伸阅读