防抖 Debouncing 完整指南从一次真实卡顿到手写实现【免费下载链接】30-seconds-of-interviewsA curated collection of common interview questions to help you prepare for your next interview.项目地址: https://gitcode.com/gh_mirrors/30/30-seconds-of-interviews在 30-seconds-of-interviews 这份前端面试题库里“What is debouncing?”是 JavaScript 方向出场率最高的一道题。本文不打算先背定义而是从一个大家都踩过的现场开始一个每敲一个键就发一次请求的筛选框。从一次卡顿说起高频事件为什么打满主线程这节回答一个问题为什么朴素的事件监听器会把页面拖得这么卡想象一个商品列表的关键词筛选框input事件一触发就fetch解析响应再重新渲染列表。用户敲 react 五个字符就发了 5 个请求、重排了 5 次列表而用户真正在意的只有最后一次。scroll这类高频事件更夸张——仓库题目里给了个数字裸的 DOM 事件监听器每秒能轻松触发 20 次事件。如果在scroll监听里直接做布局计算、写存储回调本身就会一直占着主线程浏览器来不及完成一帧绘制用户看到的就是肉眼可见的卡顿。所以问题不在逻辑写错了而在逻辑跑得太多。那防抖函数凭什么让同样的逻辑只跑一次用电梯关门逻辑理解防抖原理这节回答怎么把“执行太多次”变成“只执行一次”想想电梯门快关门时只要有人再按开门键门就重新拉开、再次延迟关门直到没人再按门才真正合上。防抖借的就是这套“安静下来才动作”的思路——每来一次触发就重置定时器只有当触发停歇、连续 delay 毫秒都没有新的到来定时器才终于走完目标函数被执行一次。说白了防抖就是给函数按下“稍等”键触发来得越密执行就被推得越靠后最终整串高频触发只留下一次调用。仓库题目给出的标准实现只有十几行重点不是照抄而是搞清下面几个机制各自负责什么。手写防抖实现只讲透三个关键机制这节直接给一份可运行的完整实现然后只聚焦支撑它的三个机制不做逐行罗列。function debounce(fn, wait) { let timer null; return function (...args) { const ctx this; clearTimeout(timer); timer setTimeout(() fn.apply(ctx, args), wait); }; }机制一闭包保存“定时器句柄”。timer声明在返回函数之外活在debounce自己的作用域里。每次调用返回的函数时都能读到上一次留下的timer并把它改掉——这是“能重置”的前提。没有闭包每次调用都会拿到一个全新的timer变量上一句定时器就永远取消不了了。机制二clearTimeout 永远先跑。每次触发都先清掉上一个没到期的定时器再重新挂一个新的。只要事件还在高频到来定时器就永远在到期前被掐掉fn一次也不会执行直到事件停歇最后一次挂上的定时器顺利走完fn才执行一次。先清后挂这个顺序就是防抖的全部核心。机制三this 与参数透传。返回的函数刻意用普通函数而不是箭头函数为了捕获调用那一刻的this比如监听器里绑定的 DOM 元素再把实参存进args最后fn.apply(ctx, args)把原来的调用现场完整还原。少了这一步被包装的函数会悄悄丢掉上下文和参数。 注意这里的“内外有别”外层用普通函数捕获动态的this内层setTimeout的回调反而用箭头函数让ctx和args稳定地待在闭包里。两层搞混调用语义就变了。防抖实战给自动保存加上 800ms 缓冲这节用一个真实业务场景说明防抖到底省掉了什么。表单页的“草稿自动保存”很典型用户停止输入后把最新内容提交到服务端一次就够了。const editor document.querySelector(#draft); const saveDraft debounce(() { fetch(/api/draft, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ content: editor.value }), }); }, 800); editor.addEventListener(input, saveDraft);不加防抖时用户连敲 50 个字就冲出去 50 个 POST服务端收到一堆中间状态绝大多数没有意义加上 800ms 缓冲后通常只有 1 个请求落地而且就是最终状态。省下的不只是带宽更重要的是消除了“旧响应覆盖新结果”的竞态主线程也不会被反复的序列化和发送占满。scroll和resize同理想等窗口停止缩放后再重排一次布局把监听器包一层debounce就行。防抖和节流的区别一张表 三个高频追问快答这节回答面试官最爱连问的两个问题防抖和节流差在哪你会不会扩展它对比项防抖debounce节流throttle何时执行事件停歇、安静窗口过后执行一次固定间隔内最多执行一次首次触发默认不执行trailing可以立即执行一次leading窗口内执行次数无论触发多少次只留一次按保底频率持续执行适合场景输入搜索、resize 重排、自动保存滚动加载、拖拽、进度上报一句话记忆把“一段事件”压缩成一次给“执行频率”装上限三个高频追问快速过一遍leading 与 trailing 模式要哪个默认实现是 trailing——安静之后才执行。如果想要“第一次点击立即响应随后连点全部忽略”按钮防连点、轮播只上报首帧点击加一个immediate选项即可function debounce(fn, wait, immediate false) { let timer null; const run function (...args) { const callNow immediate timer null; clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) fn.apply(this, args); }, wait); if (callNow) fn.apply(this, args); }; run.cancel () { clearTimeout(timer); timer null; }; return run; }immediate true时“定时器是否存在”充当冷却期标记首次调用立即执行timer存活期间后续触发一律丢弃延迟结束后timer归位才允许下一次立即执行。cancel 取消机制别忘了清理⚠️ 这里有个坑防抖函数最好暴露一个cancel。组件卸载时挂起的定时器还在一旦触发就会操作已经不存在的 DOM。一行clearTimeout就能让它安静离场上面这段代码已经顺手带上了。this 与参数透传的坑为什么不能整体换成箭头函数外层必须用普通函数捕获动态的this内层setTimeout回调用箭头函数是为了把ctx、args钉在闭包里。顺序颠倒上下文就丢了。防抖自检三问你能现场手写吗这节不给你背清单而是把要点变成三个问题——能当场答全这道题就算过关了。自检问题应该能答出为什么页面会卡事件以 20 次/秒的密度派发回调执行太多次占满主线程防抖的灵魂是哪两行clearTimeout先清旧定时器setTimeout再挂新的只有最后一个能到期ctx/args是干什么的透传原函数的this与实参保证包装后调用语义不变顺带一提这道题在仓库里的链路是这样的题目正文存放在 questions/debouncing.md由 scripts/extract.js 拆出 Answer、Good to hear 等章节写入 data/questions.json 后交给网站前端渲染成可检索的题库。换到任何一端看答案的骨架都不变事件安静下来才执行一次。【免费下载链接】30-seconds-of-interviewsA curated collection of common interview questions to help you prepare for your next interview.项目地址: https://gitcode.com/gh_mirrors/30/30-seconds-of-interviews创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?