防抖与节流:原理与实战
滚动、输入、窗口缩放等事件都可能在极短时间内触发多次。如果在每次触发时都执行复杂逻辑(如请求接口、重排布局、复杂计算),会造成卡顿与性能浪费。
“防抖(debounce)”与“节流(throttle)”是应对这类高频事件的两种常见手段。本文主要讲清楚:
- 防抖与节流的核心区别是什么?
- 如何手写通用的 debounce / throttle 工具函数?
- 在实际业务中,各自适合什么场景?
一、高频事件带来的问题
典型高频事件:
scroll(滚动)
resize(窗口大小变化)
mousemove / pointermove
keyup / keydown / input
问题:
- 事件触发频率可能高达几十甚至上百次/秒
- 若每次都执行 DOM 操作、计算或发请求,会:
- 增大 CPU 占用
- 导致掉帧、卡顿
- 给后端带来不必要压力
这时就需要:控制函数的触发频率。
二、防抖(Debounce):只在“最后一次”执行
1. 概念
防抖:在一段连续的高频触发中,只在“最后一次触发后的 N 毫秒”才真正执行回调。
若在等待期间又触发了一次,则重新计时。
可以类比电梯:
- 人不停进电梯,电梯一直等
- 当一段时间内没人再进电梯,电梯才关门运行
2. 手写防抖函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function debounce(fn, delay = 300, options = {}) { let timer = null; const { leading = false, trailing = true } = options; let lastArgs; let lastThis; let invoked = false;
function invoke() { fn.apply(lastThis, lastArgs); invoked = true; }
const debounced = function (...args) { lastArgs = args; lastThis = this;
if (timer) clearTimeout(timer);
if (leading && !invoked) { invoke(); }
timer = setTimeout(() => { if (trailing && (!leading || invoked)) { invoke(); } timer = null; invoked = false; }, delay); };
debounced.cancel = () => { if (timer) clearTimeout(timer); timer = null; invoked = false; };
return debounced; }
|
3. 使用场景
适用于“只需在用户行为结束后执行一次”的场景:
- 输入框搜索联想(停止输入 Nms 后发请求)
- 窗口大小改变后重新布局(停止拖拽窗口后计算)
- 表单实时校验(输入稳定后校验)
1 2 3 4 5
| const handleSearch = debounce((value) => { }, 300);
input.addEventListener("input", (e) => handleSearch(e.target.value));
|
三、节流(Throttle):每隔一段时间最多执行一次
1. 概念
节流:在连续触发的过程中,保证在每个时间窗口内,回调最多只执行一次。
可以类比空调压缩机:
- 不会每一秒都启动/停止,而是一定时间内控制一次,避免过度频繁切换。
2. 时间戳版节流
1 2 3 4 5 6 7 8 9 10 11
| function throttle(fn, wait = 300) { let lastTime = 0;
return function (...args) { const now = Date.now(); if (now - lastTime >= wait) { lastTime = now; fn.apply(this, args); } }; }
|
特点:
- 触发时立即执行一次(leading = true)
- 最后一次触发之后,可能不会再执行(trailing = false)
3. 定时器版节流
1 2 3 4 5 6 7 8 9 10 11
| function throttleTimer(fn, wait = 300) { let timer = null;
return function (...args) { if (timer) return; timer = setTimeout(() => { timer = null; fn.apply(this, args); }, wait); }; }
|
特点:
- 第一次触发不会立刻执行(leading = false)
- 在触发结束后还能留有一次执行(trailing = true)
4. 综合版节流(支持 leading / trailing)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function throttle(fn, wait = 300, options = {}) { let timer = null; let lastTime = 0; const { leading = true, trailing = true } = options;
const throttled = function (...args) { const now = Date.now();
if (!lastTime && !leading) { lastTime = now; }
const remaining = wait - (now - lastTime); const context = this;
if (remaining <= 0 || remaining > wait) { if (timer) { clearTimeout(timer); timer = null; } lastTime = now; fn.apply(context, args); } else if (!timer && trailing) { timer = setTimeout(() => { lastTime = leading ? Date.now() : 0; timer = null; fn.apply(context, args); }, remaining); } };
throttled.cancel = () => { if (timer) clearTimeout(timer); timer = null; lastTime = 0; };
return throttled; }
|
5. 使用场景
适用于“持续反馈,但要控制频率”的场景:
- 滚动监听(如滚动加载、吸顶导航)
- 页面 resize 时实时展示尺寸(但不必每次都更新)
- 拖拽过程中的计算与渲染
1 2 3 4 5
| const onScroll = throttle(() => { console.log(window.scrollY); }, 200);
window.addEventListener("scroll", onScroll, { passive: true });
|
四、防抖 vs 节流:如何选择?
用一句话概括:
- 防抖:一段时间内多次触发,只在“最后一次结束后”执行(如搜索输入)
- 节流:一段时间内多次触发,按固定节奏间隔执行(如滚动、拖拽)
1. 行为对比图(文字版)
- 防抖(300ms):
- 用户停止操作 300ms 后执行一次
- 期间再次触发会重置计时
- 节流(300ms):
2. 常见选择建议
- 输入框搜索、表单实时校验 → 防抖(减少无效请求)
- 滚动监听、页面 resize → 节流(持续反馈,但不过于频繁)
- 如果既想有第一次的“即时响应”,又想在结束后再执行一次,可以结合 leading/trailing 选项调整。
五、在框架中的使用(以 React/Vue 为例)
1. React 中使用防抖/节流
可配合 useCallback、useRef 封装,或使用现成库(如 lodash)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { useMemo } from "react"; import { debounce } from "lodash-es";
function SearchInput() { const debouncedSearch = useMemo( () => debounce((value) => { }, 300), [] );
return ( <input onChange={(e) => debouncedSearch(e.target.value)} placeholder="搜索..." /> ); }
|
2. Vue 中使用
可在 setup 中使用 ref 保存防抖/节流函数,或直接在 methods 中绑定。
1 2 3 4 5 6 7 8 9 10 11
| import { debounce } from "lodash-es";
export default { setup() { const handleInput = debounce((value) => { }, 300);
return { handleInput }; }, };
|
六、工程实践建议
- 把 debounce / throttle 封装成通用工具,避免每个组件手写一遍。
- 使用第三方库时,要注意:
- 在组件卸载时调用
cancel(),避免内存泄漏或在已卸载组件上 setState。
- 对滚动监听等高频事件,配合
passive: true 提升性能。
- 根据业务 UX 需求合理选择:
- “输入完再触发” → 防抖
- “持续反馈” → 节流
七、总结
防抖与节流是前端性能优化中最基础、最常用的两种手段:
- 防抖(Debounce):高频触发中,只在结束后等待一段时间再执行一次。
- 节流(Throttle):高频触发中,按照固定时间间隔执行。
掌握它们的原理与手写实现,不仅能写出更优雅、高性能的交互逻辑,也能在面试中从容应对相关考点。