面试官:“你说懂防抖?那聊聊它是怎么给服务器‘减压’的”

1,057 阅读8分钟

防抖是很多大厂面试的高频问点,为啥呢?因为这一个问题就包含许多个知识点,运用到了我们之前讲的很多细节知识,把它们串起来了。那今天就一起会会这个防抖

前言

你有没有过这种抓狂经历:填完长长的表单,点了 “提交” 按钮没反应,以为网络卡了,于是疯狂点了五六下 —— 结果收到 3 条 “提交成功” 的通知,后台还多了好几个重复订单;或者在搜索框搜 “2025 最新旅游攻略”,刚敲了个 “2”,浏览器就偷偷发了 3 次请求,敲完 “2025” 已经发了 8 次请求 —— 这波操作下来,服务器 CPU 都要 “嗡嗡” 过载,数据库还可能出现重复数据,妥妥的 “好心办坏事”!而防抖,就是专治这种 “频繁触发” 的 “服务器减压神器”,今天咱们把它扒得明明白白,连面试官爱揪的细节都不放过。

一、防抖到底是啥?——“再等等,没新动作我才真干活”

先抛核心定义(面试直接背这句):防抖(Debounce)是一种事件触发优化机制:当事件被频繁触发时,它会忽略中间的触发行为,只在 “最后一次触发后,等待指定时间内没有新触发” 时,才执行一次目标函数

用大白话翻译就是:给事件加个 “冷静期”,只要在冷静期内又触发了,就重新算冷静期,直到彻底冷静下来,才真正执行任务。

举两个生活例子理解:

  1. 电梯关门:你按下关门键(第一次触发),电梯会等 3 秒再关(冷静期);如果这 3 秒内又有人按了关门键(再次触发),电梯会重新等 3 秒 —— 直到 3 秒内没人再按,才会真正关门。这里的 “关门” 就是要执行的目标函数,“3 秒” 就是防抖的等待时间。
  2. 快递取件:你买了 5 个快递,快递员不会送一个打电话通知一次(频繁触发),而是等 5 个快递都到齐了(最后一次触发后,没有新快递),才给你打一次电话(执行通知函数)—— 这也是防抖的逻辑。

对应到咱们开头的场景:

  • 表单提交:点击按钮后,防抖设置 1 秒冷静期,1 秒内再点就重新计时,直到 1 秒内没点击,才发一次提交请求;
  • 搜索框输入:用户输入时,每输一个字符都算一次触发,防抖设置 500 毫秒冷静期,只要用户 500 毫秒内没再输入,就只发一次搜索请求。

二、代码里的防抖长啥样?(面试官必看手写版 + 逐行拆解)

直接上实战代码,这是前端面试高频手写题,咱们逐行拆,连隐藏坑都不放过:

<!-- 表单提交按钮 -->
<button id="submitBtn">提交订单</button>
<script>
    // 1. 真正要执行的核心业务逻辑(比如发请求提交订单)
    function submitOrder(e) {
        console.log('正在提交订单...', '事件对象:', e, '当前触发元素:', this);
        // 实际项目中这里会写:axios.post('/submit', 表单数据)
    }
    
    // 2. 获取按钮元素,给按钮绑定点击事件(注意:绑定的是防抖处理后的函数)
    const submitBtn = document.getElementById('submitBtn');
    // 第三个参数1000:防抖的等待时间(1秒),可根据需求调整
    submitBtn.addEventListener('click', debounce(submitOrder, 1000));

    // 3. 防抖核心函数(面试手写重点!)
    function debounce(fn, wait) {
        // 关键:闭包保存计时器ID,让每次触发都能访问到同一个timer
        let timer = null;

        // 返回一个“包装函数”,每次点击按钮实际触发的是这个函数
        return function(...args) {
            // 第一步:只要新触发了事件,就先清除之前的计时器(重置冷静期)
            // 比如第一次点击后,timer是1秒后执行的计时器;0.5秒后再点击,就把这个计时器清掉,重新算1秒
            clearTimeout(timer);

            // 第二步:重新设置计时器,等待wait时间后执行目标函数fn
            timer = setTimeout(() => {
                // 关键细节:用call改变fn的this指向,让它指向原触发元素(这里是按钮)
                // ...args:把事件对象e等参数传递给fn
                fn.call(this, ...args);
            }, wait);
        };
    }
</script>

看着乱,没事!看完解析秒懂~

逐行拆解防抖核心(面试官最爱问的 3 个点)

1. 闭包的作用:为啥 timer 要定义在 debounce 外层?

  • 如果把 timer 写在返回的包装函数里,每次点击按钮都会创建一个新的 timer,之前的计时器根本清不掉 —— 相当于防抖失效,还是会频繁执行 submitOrder。
  • 把 timer 定义在 debounce 函数外层,利用闭包特性,让每次点击触发的包装函数,都能访问到同一个 timer 变量:第一次点击创建 timer,第二次点击能通过 clearTimeout 清除这个 timer,从而实现 “重置计时”

2. clearTimeout (timer):为啥每次触发都要清计时器?

这是防抖的 “灵魂操作”!比如用户 1 秒内点了 3 次按钮:

  • 第 1 次点击:清除 timer (此时 timer 是 null,清除无效),设置 1 秒后执行 submitOrder;
  • 第 0.5 秒点击:清除第 1 次的 timer (让它没法执行),重新设置 1 秒后执行 submitOrder;
  • 第 0.8 秒点击:清除第 2 次的 timer,再重新设置 1 秒后执行 submitOrder;
  • 之后 1 秒内没再点击:第 3 次的 timer 到时间,执行 submitOrder—— 最终只执行 1 次,完美避免重复请求。

3. fn.call (this, ...args):为啥不能直接写 fn ()?

这是新手最容易踩的坑,也是面试官爱揪的细节!

  • 如果直接写fn():submitOrder 里的this会指向window(浏览器全局对象),而不是触发事件的按钮;同时,事件对象e也传不到 submitOrder 里 —— 后续如果需要通过this获取按钮状态,或者通过e阻止默认行为,都会失效。

  • fn.call(this, ...args)

    • this:绑定到包装函数的 this,也就是原事件触发元素(这里是 submitBtn 按钮);
    • ...args:把包装函数接收的所有参数(比如事件对象 e),原封不动传递给 submitOrder,保证业务逻辑能正常获取参数。

如果你在1s内一直点击提交订单,在Console里面不会有任何结果,直到你停止这种行为(提交订单),最后才会执行。

image.png

三、重置计时:用 “简单代码” 看透防抖本质

代码里面用到了计时器重置计时的概念,我们一起来理解 “重置计时”核心

function foo() {
    var a = 1;
    console.log(a);
    a++;
}
foo();
foo();

考考你:最后输出结果是多少呢?没错,最后会输出两个1

这就和防抖里的timer逻辑完全一致:

  • 每次触发事件(调用包装函数),都会先执行clearTimeout(timer)(相当于把 a 重置为 1);
  • 再重新设置timer = setTimeout(...)(相当于重新执行 foo,a 从 1 开始);
  • 只有最后一次触发后,没有新的 “重置”(没有再调用 foo),才会让a(timer)完成后续操作(打印 1 / 执行 fn)。

如果没有 “重置”(比如去掉 clearTimeout),就相当于连续调用 foo 两次,会输出两个 1—— 也就是防抖失效,频繁触发函数。

四、实战场景:防抖到底能解决哪些问题?(面试官常问 “应用场景”)

除了开头说的表单提交、搜索框输入,这些场景也必须用防抖:

  1. 窗口 resize 事件:浏览器窗口缩放时,会频繁触发 resize,如果在里面写 DOM 计算(比如重新布局),会导致页面卡顿 —— 用防抖设置 300 毫秒,缩放停止后再计算一次即可;
  2. 滚动事件(scroll) :监听页面滚动时,频繁触发 scroll 会影响性能(比如滚动加载、滚动定位),防抖后只在滚动停止后执行一次;
  3. 输入框验证:比如手机号输入验证,不用输一个数字就验证一次,等用户输完(500 毫秒没输入)再验证,减少不必要的计算;
  4. 按钮点击(比如支付、删除) :防止用户快速点击导致重复操作(重复支付、重复删除),防抖是最直接的解决方案。

五、面试避坑:这些细节能让你加分

  1. 等待时间的选择:不是固定值,要根据场景调整 —— 搜索框适合 300-500 毫秒(用户输入间隙短),表单提交适合 1000 毫秒(用户点击间隔长);
  2. 立即执行版防抖:有些场景需要 “第一次触发就执行,之后频繁触发不执行”(比如搜索框 “取消搜索” 按钮),可以给 debounce 加个参数immediate,控制是否立即执行 (面试时提一嘴,说明你考虑全面)
  3. 防抖的局限性:防抖适合 “只需要最后一次结果” 的场景,如果需要 “固定时间内必须执行一次”(比如滚动加载时,即使一直在滚动,也要每隔 1 秒加载一次),就需要用节流(Throttle)—— 但今天咱们只聊防抖,节流下次再细拆。

总结:防抖的核心逻辑一句话记

“频繁触发就重置,冷静下来再执行”—— 通过闭包保存计时器,每次触发清除旧计时器、创建新计时器,只在最后一次触发的冷静期结束后,执行一次目标函数,从而减少无效触发,给服务器 “减压”,也提升用户体验。

如果面试官让你手写防抖,直接把上面的代码写出来,再逐行解释闭包、this 绑定、参数传递这三个点,基本就能拿到满分啦!

节流下次揭晓~