ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

防抖 Debouncing 深度解析:一个定时器如何吞掉 20 次调用

防抖 Debouncing 深度解析:一个定时器如何吞掉 20 次调用

防抖 Debouncing 深度解析:一个定时器如何吞掉 20 次调用

【免费下载链接】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

搜索框每按一个键就发一次请求、滚动监听每秒触发 20 多次重活还在傻跑——这是前端高频事件的常见事故现场。防抖(Debouncing)就是为此而生的:事件不停,代码不跑;事件一停,才执行一次。开源面试题库 30-seconds-of-interviews 用一份 Markdown 题目文件就把这套机制讲完了,下面从事故现场到仓库管线完整走一遍。

先花 30 秒拿住核心

  1. 防抖的本质:把时间窗口内的多次连续调用"合并"成窗口结束后的一次执行,裸 DOM 事件监听器每秒可以轻松触发 20+ 次,防抖版只在延迟期满后调用一次;
  2. 全部实现就两个 API:clearTimeout+setTimeout,外加一个用闭包(Closure)保存的定时器句柄;
  3. 判断标准:事件还在密集触发,定时器永远到不了期;事件停了,最后一个定时器走完,目标函数执行恰好一次;
  4. 经典落点:搜索输入"停键后才发 API 请求"、scroll/resize处理降频。

机制对读:定时器被反复"清零"的游戏

想象一个带自动闭门器的门。你每推它一次,闭门器就重新归位、重新计时;只有当你有一段时间不推门,它才"唰"地关上。防抖就是给函数装了这么个闭门器:

  1. 第 1 次调用:启动一个delay毫秒的定时器;
  2. delay内又来第 2 次调用:先clearTimeout掐掉上一个,再重新计时;
  3. 事件持续到来,定时器被反复清零,目标函数一次都没跑;
  4. 事件停止,最后一个定时器独自走完——函数执行恰好一次,且拿到的是最后一次调用的参数。

关键在于"定时器句柄存在哪":它必须在返回函数的外层作用域里(闭包),下一次调用才拿得到上一次的句柄去清除。句柄若声明在内层,每次调用都是一个新值,clearTimeout永远清不到东西,合并能力直接失效。

动手实现:12 行可跑的防抖

下面是仓库题目给出的标准实现(原样引用,注意原文addEventListere为拼写错误,应为addEventListener):

const debounce = (func, delay) => { let debounceTimer; // 闭包:保存定时器句柄 return function() { const context = this; // 捕获调用时的 this const args = arguments; // 捕获全部实参 clearTimeout(debounceTimer); // 掐掉上一次未到的定时器 debounceTimer = setTimeout(() => func.apply(context, args), delay); } } window.addEventListener('scroll', debounce(function() { // Do stuff, this function will be called after a delay of 1 second }, 1000));

逐行说人话:

  • let debounceTimer:声明在返回函数之外,靠闭包跨调用存活,是"重置"能力的前提;
  • clearTimeout必须在setTimeout之前执行,顺序反了就白搭;
  • context/args加上func.apply:包装层原样透传调用现场,不改写目标函数的语义;
  • 用法上:debounce(fn, 1000)返回的是新的包装函数,挂到scroll上的是它,1 秒后用户停止滚动,真实回调才执行一次。

工程账本:优化前 vs 优化后

以"输入 react 这 5 个字符,每键发一次搜索请求"为例:

维度不加防抖300ms 防抖
网络请求次数5 次1 次
用户体感连续刷新结果,旧响应可能覆盖新结果停顿后出一次结果,稳定
主线程负担每次按键都渲染仅末态渲染一次

再对照滚动场景:题目里明确裸监听器"每秒可触发 20+ 次",防抖后一段连续滚动只结算一次。两个场景共同指向同一条原则——以末态为准的事(末次搜索词、末次视口尺寸),交给防抖最划算。

追问时间:面试官真正想听的部分

追问 1:防抖 vs 节流,边界在哪?

对比项防抖(debounce)节流(throttle)
执行时机调用停止后延迟执行一次固定间隔内至多一次
控制对象窗口内的调用次数执行的频率上限
典型场景搜索、resize 后重排拖拽、滚动进度、游戏循环
末态保证保证用最后一次参数不保证,可能丢末态

记忆锚点:要"末态"选防抖,要"保底频率"选节流。

追问 2:this和参数为什么单独接住?外层return function()必须是普通函数而非箭头函数——事件监听器里this是动态的(窗口或绑定元素),箭头函数会把它锁死成定义时的作用域;arguments则原样接住所有实参,最后经func.apply一次性透传。

追问 3:首次调用能立即执行吗?能,这就是 leading edge 模式:首次调用直接执行,之后进入冷却期忽略后续调用,delay结束才解锁。但仓库给出的标准实现是 trailing(末次执行),面试先答这个,再主动补一句"需要首触立即执行时可用 leading 版",层次就出来了。

项目视角:这道题在仓库里走了哪条链路

  • questions/debouncing.md——内容源。含#### Answer、#### Good to hear等固定小节(格式约定见 question-template.md),另有<!-- tags: -->、<!-- expertise: -->隐藏注释承载标签与难度;
  • scripts/util.js——解析工具层。readQuestions()按字母序读取整个questions/目录,getSection()按标题切块,getCodeBlocks()抽取代码块;
  • scripts/extract.js——管线核心。把每题的 question / answer / goodToHear / 难度 / 代码块组装成对象写入data/questions.json,对应package.json里的"extractor": "node ./scripts/extract.js";
  • data/questions.json——数据文件,前端唯一数据源,由 website/js/state.js 直接 import 进 Hyperapp 状态;
  • website/js/components/——展示层:Filter.js管标签过滤,Questions.js负责按难度排序渲染列表,Question.js展开单题,Markdown.js用 marked 渲染答案、Prism 高亮代码。

一条冷知识可以加分:当前提交的data/questions.json只有 103 道题,而questions/目录已有 109 个 md 文件,debouncing.md还没进数据——这道题是在数据快照之后加入的,重跑一次"extractor"脚本它就会出现在网站上。

自测卡片

  1. 防抖一次触发里,clearTimeout和setTimeout各执行几次?
  2. 持续滚动 3 秒后停止(delay=1000ms),回调执行几次?
  3. 外层返回箭头函数会丢掉什么?
  4. 需要"保证每 100ms 至少执行一次"的场景,该选防抖还是节流?

关键词收束:防抖(Debouncing)、clearTimeout+setTimeout、闭包状态、末态合并、防抖和节流的区别(debounce vs throttle)。

【免费下载链接】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),仅供参考

返回列表