创见博客
防抖与节流:实现、使用场景与踩坑
七崽爱吃小饼干2026/10/07阅读 0

搜索框每敲一个字就发一次请求,滚动到底部疯狂触发加载——高频率触发的交互,如果不加控制,轻则卡顿,重则把后端打挂。防抖和节流就是解决这类问题的两把梳子,区别只在于"什么时候真正执行"。

js
// 输入框:停止输入 500ms 后才查一次
input.addEventListener('input', debounce(search, 500))

// 滚动:每 200ms 最多执行一次
window.addEventListener('scroll', throttle(onScroll, 200))

这篇先讲原理和手写实现,再对比适用场景,最后说几个容易踩的坑。

一、防抖:等你停下来

防抖(debounce) 的核心是:事件触发后不立即执行,而是等安静一段时间后再执行。如果这期间又触发了,就重新计时。

用生活类比:电梯门。不断有人进来就不断重新开始关门倒计时,直到没人了才关门。

基础实现

js
function debounce(fn, delay = 300) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

使用:

js
const onSearch = debounce((keyword) => {
  console.log('发起请求:', keyword)
}, 500)

onSearch('a')
onSearch('ab')
onSearch('abc')
// 只有最后一次 'abc' 会在 500ms 后执行

立即执行版(trailing: false)

有些场景希望第一次立刻响应,之后才进入冷却,比如按钮点击防止重复提交:

js
function debounce(fn, delay = 300, immediate = false) {
  let timer = null
  return function (...args) {
    if (timer) clearTimeout(timer)

    if (immediate && !timer) {
      fn.apply(this, args)
    }

    timer = setTimeout(() => {
      timer = null
      if (!immediate) {
        fn.apply(this, args)
      }
    }, delay)
  }
}

二、节流:按固定频率执行

节流(throttle) 的核心是:保证在一段时间内最多执行一次。不管触发多频繁,执行频率都被"限流"。

生活类比:地铁发车。每 5 分钟一班,这期间来多少乘客都等下一班。

时间戳版

第一次立即执行,停止触发后不再补执行:

js
function throttle(fn, interval = 300) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last >= interval) {
      last = now
      fn.apply(this, args)
    }
  }
}

定时器版

第一次延迟执行,停止触发后还会补执行最后一次:

js
function throttle(fn, interval = 300) {
  let timer = null
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, interval)
  }
}

合并版(首尾都执行)

实际项目里常用这个,既有立即响应,也保证最后一次不丢:

js
function throttle(fn, interval = 300) {
  let last = 0
  let timer = null
  return function (...args) {
    const now = Date.now()
    const remaining = interval - (now - last)

    if (remaining <= 0) {
      // 距离上次执行已超过间隔,立即执行
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      last = now
      fn.apply(this, args)
    } else if (!timer) {
      // 否则安排一次尾部执行
      timer = setTimeout(() => {
        last = Date.now()
        timer = null
        fn.apply(this, args)
      }, remaining)
    }
  }
}

三、两者到底差在哪

维度防抖 debounce节流 throttle
执行时机停止触发后延迟执行固定间隔内最多执行一次
触发频率高时只执行最后一次按频率周期性执行
有无"补执行"结束才执行视实现而定
典型场景搜索、校验、resize 结束滚动、拖拽、mousemove

一句话:防抖关心"最终结果",节流关心"过程节奏"。

  • 你只在乎用户最终输入的是什么 → 防抖。
  • 你需要在滚动过程中持续(但不能太频繁)地反馈 → 节流。

四、使用场景对照

适合防抖:

  • 搜索框实时联想:等用户停手再发请求。
  • 表单字段校验:输入完成后校验一次,而不是每敲一个字。
  • resize 结束后重新计算布局。
  • 按钮点击防重复提交(立即执行版)。

适合节流:

  • 页面滚动加载 / 监听滚动位置(如吸顶导航)。
  • 鼠标移动 mousemove、拖拽。
  • 游戏里的射击频率、高频上报埋点。
js
// 滚动吸顶:节流,保持过程顺滑
window.addEventListener('scroll', throttle(() => {
  const scrolled = window.scrollY > 100
  header.classList.toggle('fixed', scrolled)
}, 100))

// 窗口调整:防抖,只处理最终尺寸
window.addEventListener('resize', debounce(() => {
  chart.resize()
}, 200))

五、几个容易踩的坑

1. 注意 this 和参数透传

包装函数里必须 fn.apply(this, args),否则原函数里的 this 会变成 window,事件对象等参数也会丢。

2. 返回值和取消

基础版拿不到原函数的返回值,也无法中途取消。生产环境建议支持 cancel:

js
function debounce(fn, delay = 300) {
  let timer = null
  function debounced(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }
  return debounced
}

组件卸载时调用 cancel(),避免定时器在组件已销毁后仍触发,导致内存泄漏或报错。

3. React 中不要每次渲染都新建

js
// ❌ 每次渲染都生成新函数,防抖失效
function Search() {
  const onChange = debounce(handleSearch, 500)
  return <input onChange={onChange} />
}

// ✅ 用 useMemo / useRef 固定引用,卸载时清理
function Search() {
  const debouncedRef = useRef(null)
  if (!debouncedRef.current) {
    debouncedRef.current = debounce(handleSearch, 500)
  }
  useEffect(() => () => debouncedRef.current.cancel(), [])
  return <input onChange={(e) => debouncedRef.current(e.target.value)} />
}

4. 选间隔要贴合场景

延迟太大用户会觉得"没反应",太小又起不到限流效果。搜索建议 300ms 左右,滚动 100~200ms,按体验实测调整。

小结

  • 防抖:等安静后执行,只认最后一次,适合"结果导向"的输入类交互。
  • 节流:固定频率执行,适合"过程导向"的滚动、拖拽类交互。
  • 手写时务必透传 this 和参数,并提供 cancel 以便清理。
  • 框架里要用 useMemo / useRef 固定引用,并在卸载时取消,否则防抖会失效或泄漏。
评论
0/100