防抖与节流
什么时候用谁
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 搜索框联想、表单实时校验 | 防抖(debounce) | 用户连续输入时只触发最后一次,减少无效请求 |
| 滚动监听、窗口 resize、拖拽跟随 | 节流(throttle) | 高频事件按固定节奏执行,保证流畅度和稳定性 |
| 按钮防重复点击(提交、支付) | 防抖(leading) | 立刻响应一次,然后短时间内忽略重复点击 |
简单来说: 防抖是等你 "停下来" 再执行,节流是不管你停不停,都 "按频率" 执行
防抖(Debounce)
防抖的核心是:在 wait 时间内如果又触发了,就重新计时
准备一个
timer容器防抖一定要有 "上一次任务" 的句柄,这样下一次触发时才能取消它
let timer: ReturnType<typeof setTimeout> | null = null每次触发先清理上一次任务
if (timer) { clearTimeout(timer) }在等待时间结束后再执行业务逻辑(搜索联想)
base.tsconst input = document.querySelector<HTMLInputElement>('#keyword')! const list = document.querySelector<HTMLUListElement>('#result')! let timer: ReturnType<typeof setTimeout> | null = null let controller: AbortController | null = null async function fetchSuggest(keyword: string) { // 新请求发起前,取消上一个未完成请求 controller?.abort() controller = new AbortController() const res = await fetch(`/api/suggest?q=${encodeURIComponent(keyword)}`, { signal: controller.signal, }) const data = await res.json() list.innerHTML = data.map((item: string) => `<li>${item}</li>`).join('') } input.addEventListener('input', (e) => { const keyword = (e.target as HTMLInputElement).value.trim() if (timer) { clearTimeout(timer) } timer = setTimeout(() => { if (!keyword) { list.innerHTML = '' return } fetchSuggest(keyword) }, 300) })需要 "立即执行一次" 的场景,使用冷却锁(按钮防重复提交)
const submitBtn = document.querySelector<HTMLButtonElement>('#submit')! let cooling = false async function submitOrder() { await fetch('/api/order', { method: 'POST' }) } submitBtn.addEventListener('click', async () => { if (cooling) { return } cooling = true try { await submitOrder() } finally { setTimeout(() => { cooling = false }, 2000) } })完整代码
const input = document.querySelector<HTMLInputElement>('#keyword')! const list = document.querySelector<HTMLUListElement>('#result')! const submitBtn = document.querySelector<HTMLButtonElement>('#submit')! let timer: ReturnType<typeof setTimeout> | null = null let controller: AbortController | null = null let cooling = false async function fetchSuggest(keyword: string) { controller?.abort() controller = new AbortController() const res = await fetch(`/api/suggest?q=${encodeURIComponent(keyword)}`, { signal: controller.signal, }) const data = await res.json() list.innerHTML = data.map((item: string) => `<li>${item}</li>`).join('') } async function submitOrder() { await fetch('/api/order', { method: 'POST' }) } input.addEventListener('input', (e) => { const keyword = (e.target as HTMLInputElement).value.trim() if (timer) { clearTimeout(timer) } timer = setTimeout(() => { if (!keyword) { list.innerHTML = '' return } fetchSuggest(keyword) }, 300) }) submitBtn.addEventListener('click', async () => { if (cooling) { return } cooling = true try { await submitOrder() } finally { setTimeout(() => { cooling = false }, 2000) } })
节流(Throttle)
节流的核心是:高频触发时,按固定时间片执行
定义节流窗口(
wait)和执行时间戳(lastTime)const wait = 200 let lastTime = 0每次触发都判断 "是否到间隔"
const now = Date.now() if (now - lastTime < wait) { return } lastTime = now到达间隔就执行逻辑(滚动监听)
const wait = 200 let lastTime = 0 window.addEventListener('scroll', () => { const now = Date.now() if (now - lastTime < wait) { return } lastTime = now const scrollTop = window.scrollY const reachBottom = window.innerHeight + scrollTop >= document.body.offsetHeight - 50 if (reachBottom) { console.log('load next page') } })需要 "最后一次也执行" 时,增加 trailing 逻辑(resize)
const wait = 300 let lastTime = 0 let timer: ReturnType<typeof setTimeout> | null = null function runResizeTask() { console.log('re-calc layout', window.innerWidth) } window.addEventListener('resize', () => { const now = Date.now() const remain = wait - (now - lastTime) if (remain <= 0) { if (timer) { clearTimeout(timer) timer = null } lastTime = now runResizeTask() return } if (!timer) { timer = setTimeout(() => { timer = null lastTime = Date.now() runResizeTask() }, remain) } })完整代码
const SCROLL_WAIT = 200 const RESIZE_WAIT = 300 let lastScrollTime = 0 let lastResizeTime = 0 let resizeTimer: ReturnType<typeof setTimeout> | null = null function handleScroll() { const now = Date.now() if (now - lastScrollTime < SCROLL_WAIT) { return } lastScrollTime = now const scrollTop = window.scrollY const reachBottom = window.innerHeight + scrollTop >= document.body.offsetHeight - 50 if (reachBottom) { console.log('load next page') } } function runResizeTask() { console.log('re-calc layout', window.innerWidth) } function handleResize() { const now = Date.now() const remain = RESIZE_WAIT - (now - lastResizeTime) if (remain <= 0) { if (resizeTimer) { clearTimeout(resizeTimer) resizeTimer = null } lastResizeTime = now runResizeTask() return } if (!resizeTimer) { resizeTimer = setTimeout(() => { resizeTimer = null lastResizeTime = Date.now() runResizeTask() }, remain) } } window.addEventListener('scroll', handleScroll) window.addEventListener('resize', handleResize)
