防抖与节流:实现、使用场景与踩坑
七崽爱吃小饼干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固定引用,并在卸载时取消,否则防抖会失效或泄漏。