防抖与节流:原理、实现与应用场景 防抖(Debounce) 核心原理 在事件被触发 n 秒后再执行回调,若在 n 秒内事件再次被触发,则重新计时 确保函数只在最后一次触发后执行一次
使用场景
场景
说明
搜索框输入
用户停止输入后再发送请求,避免频繁请求
窗口大小调整
窗口调整结束后再计算布局,避免重复渲染
表单验证
用户输入完成后再进行验证,减少实时验证开销
按钮防重
防止用户快速重复点击导致多次提交
实时保存
文档编辑时,停止输入一段时间后自动保存
基础实现 1 2 3 4 5 6 7 8 9 function debounce (fn, delay ) { let timer = null ; return function (...args ) { clearTimeout (timer); timer = setTimeout (() => { fn.apply (this , args); }, delay); }; }
增强版(支持立即执行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function debounce (fn, delay, immediate = false ) { let timer = null ; return function (...args ) { clearTimeout (timer); if (immediate && !timer) { fn.apply (this , args); } timer = setTimeout (() => { if (!immediate) { fn.apply (this , args); } timer = null ; }, delay); }; }
应用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const searchInput = document .getElementById ('search' );const handleSearch = debounce (query => { console .log (`搜索: ${query} ` ); }, 500 ); searchInput.addEventListener ('input' , e => handleSearch (e.target .value )); const handleResize = debounce (() => { console .log ('窗口调整完成' ); }, 300 ); window .addEventListener ('resize' , handleResize);
节流(Throttle) 核心原理 在单位时间内,无论事件触发多少次,只执行一次回调函数 确保函数以固定频率执行
使用场景
场景
说明
滚动事件
滚动页面时触发加载更多内容,避免高频执行
鼠标移动
跟踪鼠标位置,限制触发频率
游戏操作
限制角色移动或攻击频率
高频点击
限制按钮点击频率,防止误操作
动画渲染
保证动画帧率稳定,避免过度渲染
时间戳实现 1 2 3 4 5 6 7 8 9 10 function throttle (fn, interval ) { let lastTime = 0 ; return function (...args ) { const now = Date .now (); if (now - lastTime >= interval) { fn.apply (this , args); lastTime = now; } }; }
定时器实现(支持尾执行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function throttle (fn, interval ) { let timer = null ; let lastTime = 0 ; return function (...args ) { const now = Date .now (); const remaining = interval - (now - lastTime); clearTimeout (timer); if (remaining <= 0 ) { fn.apply (this , args); lastTime = now; } else { timer = setTimeout (() => { fn.apply (this , args); lastTime = Date .now (); }, remaining); } }; }
应用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let page = 1 ;const loadMore = throttle (() => { const scrollBottom = document .documentElement .scrollHeight - window .innerHeight - window .scrollY ; if (scrollBottom < 500 ) { console .log (`加载第 ${page} 页数据` ); page++; } }, 500 ); window .addEventListener ('scroll' , loadMore);const playerAttack = throttle (() => { console .log ('玩家发动攻击' ); }, 1000 ); attackButton.addEventListener ('click' , playerAttack);
对比总结
特性
防抖
节流
核心思想
延迟执行,只执行最后一次
固定频率执行
执行时机
停止触发后执行
间隔时间内执行
执行次数
多次触发只执行一次
多次触发按固定频率执行
实现关键
setTimeout + clearTimeout
时间戳或定时器
适用场景
输入类事件
高频触发事件
用户感知
响应最后操作结果
保持操作流畅性
可视化对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 graph LR A[事件触发] --> B{防抖模式} A --> C{节流模式} B --> D[触发1] D --> E[重置计时] E --> F[触发2] F --> G[重置计时] G --> H[触发3] H --> I[执行回调] C --> J[触发1-立即执行] J --> K[冷却期] K --> L[触发2-忽略] L --> M[冷却结束] M --> N[触发3-执行]
现代开发实践 使用 Lodash 实现 1 2 3 4 5 6 7 8 9 10 11 import { debounce, throttle } from 'lodash' ;const searchHandler = debounce (query => { }, 500 ); const scrollHandler = throttle (() => { }, 300 );
React Hooks 封装 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 function useDebounce (callback, delay ) { const callbackRef = useRef (callback); useEffect (() => { callbackRef.current = callback; }); return useMemo (() => { const handler = (...args ) => { clearTimeout (handler.timeout ); handler.timeout = setTimeout (() => { callbackRef.current (...args); }, delay); }; return handler; }, [delay]); } function useThrottle (callback, interval ) { const callbackRef = useRef (callback); const lastTimeRef = useRef (0 ); useEffect (() => { callbackRef.current = callback; }); return useCallback ((...args ) => { const now = Date .now (); if (now - lastTimeRef.current >= interval) { callbackRef.current (...args); lastTimeRef.current = now; } }, [interval]); }
性能优化建议
合理设置延迟时间 :防抖250-500ms,节流100-300ms
避免过度使用 :只在必要时应用
及时清理 :组件卸载时清除定时器
结合RAF :动画场景使用requestAnimationFrame
1 2 3 4 5 6 7 8 9 10 11 12 function rafThrottle (fn ) { let ticking = false ; return function (...args ) { if (!ticking) { requestAnimationFrame (() => { fn.apply (this , args); ticking = false ; }); ticking = true ; } }; }