防抖(Debounce)与节流(Throttle)面试题全解

📅 2026/8/22 11:27:18
防抖(Debounce)与节流(Throttle)面试题全解
防抖Debounce与节流Throttle面试题全解一、核心思路一句话防抖是等你不动了再执行节流是固定频率匀速执行——本质都是对高频事件的时间维度降频区别在于重置还是锁定。二、解决方案架构图文本版┌─────────────────────────────────────────────────────────┐ │ 高频事件触发scroll/input/resize │ └──────────────────────┬──────────────────────────────────┘ │ ┌────────────┴────────────┐ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ 防抖 Debounce │ │ 节流 Throttle │ │ (重置计时器) │ │ (时间戳/锁) │ └──────┬──────┘ └──────┬──────┘ │ │ ▼ ▼ 停止触发 wait 后 每 wait 时间窗口 执行最后一次调用 最多执行一次 │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ 适用搜索输入、│ │ 适用滚动加载、│ │ resize、表单校验│ │ 拖拽、按钮防连点│ └─────────────┘ └─────────────┘三、第一层核心区别概念辨析维度防抖 Debounce节流 Throttle核心机制每次触发→清空旧定时器→重设新定时器每次触发→判断距上次执行是否超过间隔→决定执行或丢弃执行时机停止触发后 wait ms 才执行trailing或首次立即执行leading按固定频率执行如每 300ms 最多一次类比电梯关门有人进来就重新等没人了才关门红绿灯固定周期放行不管来了多少人触发结果N 次触发 →1 次执行最后一次N 次触发 →⌈N/间隔⌉ 次执行主要矛盾 vs 次要矛盾说明主要矛盾高频事件导致性能瓶颈DOM 操作/网络请求/重排重绘需要降频次要矛盾用户体验响应延迟vs 性能执行频率的平衡边界场景首次/末次是否执行四、第二层手写实现核心干货4.1 防抖 — 基础版functiondebounce(fn,wait300){lettimernull;returnfunction(...args){// 核心每次触发都清空旧定时器clearTimeout(timer);timersetTimeout((){fn.apply(this,args);// 注意 this 指向},wait);};}执行流程图触发1 → 设timer(300ms) 触发2 → 清timer → 重设timer(300ms) 触发3 → 清timer → 重设timer(300ms) ...停止触发... 300ms后 → 执行最后一次 fn4.2 防抖 — 完整版leading cancelfunctiondebounce(fn,wait300,immediatefalse){lettimernull;functiondebounced(...args){// 如果已有定时器清除if(timer)clearTimeout(timer);// leading: 首次立即执行if(immediate!timer){fn.apply(this,args);}timersetTimeout((){// trailing: 停止触发后执行immediate模式下不重复执行if(!immediate){fn.apply(this,args);}timernull;// 重置允许下次 leading},wait);}// 取消功能debounced.cancelfunction(){clearTimeout(timer);timernull;};returndebounced;}4.3 节流 — 时间戳版首次立即执行functionthrottle(fn,wait300){letlastTime0;returnfunction(...args){constnowDate.now();if(now-lastTimewait){lastTimenow;fn.apply(this,args);}};}特点首次触发立即执行末次触发可能被丢弃。4.4 节流 — 定时器版末次保证执行functionthrottle(fn,wait300){lettimernull;returnfunction(...args){if(!timer){timersetTimeout((){fn.apply(this,args);timernull;// 释放锁允许下一轮},wait);}};}特点首次不立即执行等 wait末次保证执行。4.5 节流 — 组合版首尾都执行 cancelfunctionthrottle(fn,wait300,{leadingtrue,trailingtrue}{}){lettimernull;letlastTime0;functionthrottled(...args){constnowDate.now();// leadingfalse 时首次将 lastTime 设为 now跳过首次if(!lastTime!leading)lastTimenow;constremainingwait-(now-lastTime);if(remaining0||remainingwait){// 时间到立即执行if(timer){clearTimeout(timer);timernull;}lastTimenow;fn.apply(this,args);}elseif(!timertrailing){// 未到时间用定时器保证末次执行timersetTimeout((){lastTimeleading?Date.now():0;timernull;fn.apply(this,args);},remaining);}}throttled.cancelfunction(){clearTimeout(timer);timernull;lastTime0;};returnthrottled;}4.6 对比总结时间戳版节流定时器版节流组合版节流首次执行✅ 立即❌ 等 wait可配置末次执行❌ 可能丢失✅ 保证可配置实现方式比较时间差锁 setTimeout两者结合五、第三层工程应用React Hooks 封装5.1 useDebounceimport{useRef,useCallback,useEffect}fromreact;functionuseDebounceTextends(...args:any[])any(fn:T,wait300):T{cancel:()void}{consttimerRefuseRefReturnTypetypeofsetTimeout|null(null);constfnRefuseRef(fn);// 始终引用最新 fn避免闭包陷阱useEffect((){fnRef.currentfn;});// 组件卸载时清理防止内存泄漏useEffect((){return(){if(timerRef.current)clearTimeout(timerRef.current);};},[]);constdebounceduseCallback((...args:any[]){if(timerRef.current)clearTimeout(timerRef.current);timerRef.currentsetTimeout((){fnRef.current(...args);},wait);},[wait])asT{cancel:()void};debounced.canceluseCallback((){if(timerRef.current){clearTimeout(timerRef.current);timerRef.currentnull;}},[]);returndebounced;}5.2 useThrottlefunctionuseThrottleTextends(...args:any[])any(fn:T,wait300):T{constlastRefuseRef(0);consttimerRefuseRefReturnTypetypeofsetTimeout|null(null);constfnRefuseRef(fn);useEffect((){fnRef.currentfn;});useEffect((){return(){if(timerRef.current)clearTimeout(timerRef.current);};},[]);constthrottleduseCallback((...args:any[]){constnowDate.now();constremainingwait-(now-lastRef.current);if(remaining0){lastRef.currentnow;fnRef.current(...args);}elseif(!timerRef.current){timerRef.currentsetTimeout((){lastRef.currentDate.now();timerRef.currentnull;fnRef.current(...args);},remaining);}},[wait])asT;returnthrottled;}5.3 React 中的关键陷阱问题原因解决闭包陷阱回调捕获旧 state用useRef存最新 fn重复创建每次 render 生成新函数useCallbackuseRef内存泄漏组件卸载后 timer 仍触发useEffect返回清理函数SSR 问题setTimeout在服务端行为不同判断typeof window ! undefined六、使用场景 边界场景使用场景场景选择原因搜索框输入联想防抖用户打完字才需请求中间态无意义窗口 resize 重算布局防抖只关心最终尺寸表单实时校验防抖停止输入后校验滚动加载无限列表节流滚动过程中需持续检测位置拖拽mousemove节流需要持续跟手不能等停止按钮防重复提交节流或一次性锁固定间隔内只允许一次游戏/动画帧控制节流配合 rAF匀速更新边界场景边界说明处理wait 0应同步执行还是下一个微/宏任务即使 0mssetTimeout 也是异步可用if (!wait) return fn(...)短路this 指向箭头函数丢失 this用fn.apply(this, args)保持调用者上下文返回值原函数有返回值防抖/节流后无法同步获取返回 Promise 或使用回调组件卸载定时器未清理导致 setState on unmountedcleanup 中 clearTimeout快速切换页面/Tab旧 timer 残留路由切换时调用 cancel()并发多个防抖实例共享 timer 变量确保每次调用 debounce() 生成独立闭包七、常见追问 补充知识点防抖和节流能否互相替代不能。防抖强调最终一次节流强调均匀采样语义不同。lodash 的_.debounce和_.throttle额外做了什么支持leading/trailing/maxWait、返回 Promiseflush、cancel、内部用Date.now()setTimeout组合保证精度。为什么不用setInterval实现节流setInterval 不等事件触发才计时空跑浪费资源且与事件触发时机不对齐。防抖 节流能否组合可以。例如滚动事件节流 100ms 采样停止滚动 300ms 后防抖执行最终逻辑。requestAnimationFrame算不算节流是天然的 16.7ms 节流60fps适合动画/滚动视觉更新但不适合需要精确间隔的场景。八、满分答案面试直接背诵版面试官防抖和节流的区别与手写实现是什么【概念区分 — 30秒】防抖和节流都是对高频事件的时间降频策略。防抖Debounce事件触发后等待 wait 时间若期间再次触发则重置计时直到停止触发 wait 后才执行——“多次变一次取最后一次”。节流Throttle在固定时间窗口内最多执行一次多余触发被忽略——“匀速采样固定频率”。【场景举例 — 10秒】防抖搜索输入联想、resize 重算、表单校验。节流滚动加载、拖拽跟手、按钮防连点、动画帧控制。【手写核心 — 60秒】防抖核心闭包持有timer每次调用clearTimeout后重设到期后fn.apply(this, args)。进阶加immediate参数实现 leading 模式加cancel方法。节流核心时间戳版记录lastTimenow - lastTime wait才执行——首次立即执行末次可能丢。定时器版用timer当锁无锁时设 setTimeout到期执行并释放——末次保证首次延迟。生产用组合版leading trailing 可配置 cancel。【工程落地 — 20秒】React 中封装useDebounce/useThrottle用useRef存 timer 和最新 fn避免闭包陷阱useCallback保持引用稳定useEffect清理函数中clearTimeout防内存泄漏暴露cancel方法供路由切换/组件卸载时调用。【一句话总结】防抖是重置思维——等你不动了再做节流是锁思维——到点了才放行。选型看业务要最终结果还是过程采样。以上即为该面试题的完整结构化答案覆盖概念→手写→工程→边界四层深度。