JavaScript的防抖技术:大厂的敲门砖

238 阅读7分钟

引言:为什么防抖值千金?

当你信心满满走进大厂面试间,面试官突然甩出:"手写一个防抖函数"——此刻多少人的笑容僵在脸上?防抖作为JS高频考点,早已成为区分"前端搬砖工"和"工程师"的试金石。掌握它,offer就在眼前;不会它?出门右转慢走不送。

一、致命场景:疯狂点击的毁灭力量

想象一个用户信息提交页面:

<button id="btn">提交订单</button>
<script>
  document.getElementById('btn').addEventListener('click', () => {
    console.log("💰 扣款请求已发送!");
    // 实际场景:向服务器发送支付请求
  });
</script>

当用户点击提交后:
1️⃣ 网络延迟导致页面无响应
2️⃣ 用户焦虑狂点10次提交按钮
3️⃣ 服务器收到10个重复支付请求
4️⃣ 用户投诉 + 财务对账崩溃 = 程序员祭天

二、青铜方案:禁用按钮大法

初级解决方案简单粗暴:当用户点击提交按钮时,立即禁用提交按钮,可防止用户提交重复的请求

<script>
  const btn = document.getElementById('btn');
  btn.addEventListener('click', () => {
    btn.disabled = true; // 立即锁定按钮
    console.log("🚫 按钮已禁用,请求发送中...");
    // 发送请求...
  });
</script>

禁用按钮大法 虽然简单直接且能够防止手抖党,但是这个方法有以下几个致命的缺陷

  • 请求失败后按钮永远被锁(需额外重置逻辑)
  • 用户体验极差(用户怀疑系统卡死)
  • 无法解决滚动条/输入框等非按钮场景

三、王者方案:防抖技术登场

防抖核心哲学

在事件风暴中,只执行最后一次真正的呼唤

什么是 防抖 呢? 防抖 是一种优化手段,用于控制高频事件的触发频率。其核心思想是,在规定的时间内如果事件被重复触发,则之前的触发会被取消,只有最后一次触发会在设定的时间间隔后执行。例如,在搜索框输入时,用户快速连续输入字符,每次输入都可能触发请求,但使用防抖可以避免频繁请求服务器,仅在用户停止输入一定时间后才执行搜索操作。

手搓防抖的绝句诗

首联:闭包藏器待时发(闭包保存定时器,等待时机执行)

为什么要用 闭包 来保持定时器呢?

在V8引擎中,代码执行前需先进行编译,形成全局执行上下文,随后触发函数的执行上下文。这一过程涉及创建执行上下文对象、查找行参和变量声明、统一行参和实参等步骤。函数声明和定义在特定上下文中,但可能不在同一上下文中执行。执行完毕后,执行上下文会被销毁,以避免内存溢出,保证引擎运行效率。此外V8引擎还有一条规则,根据作用域查找规则--即内部作用域能够访问外部作用域。

    <script>
        function debounce(fn,wait){
            var timer = null   //闭包藏定时器
        return function(){
        clearTimeout(timer)
        timer = setTimeout(()=>{
        fn()
        },wait)
        }
        }
    </script>

在两条规则下,debounce(fn,wait) 首先进行编译,编译完成后,执行上下文被销毁,用户点击鼠标后执行 function() ,进行预编译时,执行 clearTimeout(timer) 由于内部作用域没有 timer,则会向外作用域找,但是外作用域即 debounce(fn,wait) 的执行上下文已经被销毁。可以看出内部作用域访问外部作用域变量的规则,以及函数执行完毕后执行上下文的销毁机制的这两条规则互相冲突,所以引擎会保留一个小空间(即 闭包空间 )来存储内部函数需要访问的外部变量,以确保代码的正确运行。

颔联:破旧立新延火华(清除旧的定时器,再设新的,延迟执行如延绵的火花)

用户点击一次提交,会触发一次 function(),然后执行 clearTimeout(timer),在内部作用域找不到就会去外部作用域即function()找,但是由于执行上下文被销毁,所以只能去闭包里找,此时timer = null。 代码接着执行此时闭包中 timer = set ,当用户在 wait 中再次点击提交,又会触发一次 function(),然后执行clearTimeout(timer),在内部作用域找不到就会去外部作用域即function()找,但是由于执行上下文被销毁,所以只能去闭包里找,此时 timer = set ,执行clearTimeout(timer)将其销毁,相对于timer = null,然后执行下一行代码此时 timer = set ,所以用户在 wait 时间内点击提交 ,会将上一个旧的定时器销毁 ,然后设置新的定时器 ,这样就达到只执行最后一句呼唤 ,达到防抖效果。

颈联:传音入密参无差(参数传递要准确无误)

在没有启用防抖技术时,事件处理函数可以直接接受事件参数,事件参数包含了与事件相关的所有信息,比如事件类型、触发事件的元素、事件发生时的状态(如鼠标位置、按键状态等)等。但是在启用防抖技术后,原始事件参数面临三重封锁

  1. 屏障隔离:防抖包装函数截获事件对象
  2. 时间断层:延迟执行导致参数时效性丢失
  3. 通道变形:异步回调改变参数传递路径

在使用防抖技术前,handle(e)拥有实践参数,使用防抖技术后,变成function()拥有事件参数,所有可以通过以下代码来保证handle(e)的参数传递。

<script>
        let btn = document.getElementById('btn')
        function handle(e){
            // console.log(e.x,e.y)

             console.log('向后端发请求')
        }
        btn.addEventListener('click',debounce(handle,1000))

        function debounce(fn,wait){
            var timer = null
        return function(...arg){   //保证参数传递
        clearTimeout(timer)
        timer = setTimeout(()=>{
        fn(...arg)  //保证参数传递
        ,wait)
        }
        }
    </script>

尾联:定海神针指向家(this指向要固定,如同定海神针指向正确的家)

this 的指向:

  • 函数被独立调用, this 指向Window
  • 函数被对象调用, this 指向这个对象
  • 函数被显示调用, this 指向显示绑定的对象
  • 函数被 new 调用, this 指向实例对象

在使用防抖技术之前, this 指向 btn 。

 <script>
        let btn = document.getElementById('btn')
        function handle(e){
             console.log('向后端发请求',this)
        }
        btn.addEventListener('click',handle)
    </scrip >

在使用防抖技术后,function() 的 this 指向 btn 。handle() 被 fn() 独立调用,此时 fn() 的 this 指向 Window ,要保持 fn() 的 this 指向 btn ,则用显示绑定:将 fn() 的 this 同步 function() 的 this :将 fn() 的 this 同步 function ()的 this ,此时 fn() 的 this 指向 btn 指向正确。

    <script>
        let btn = document.getElementById('btn')
        function handle(e){
            // console.log(e.x,e.y)

             console.log('向后端发请求')
        }
        btn.addEventListener('click',debounce(handle,1000))

        function debounce(fn,wait){
            var timer = null
        return function(...arg){
        clearTimeout(timer)
        timer = setTimeout(()=>{
        fn.call(this,...arg)  //显示绑定:将fn()的this同步外一层的this即function()的this
        },wait)
        }
        }
    </script>

但是由于计数器中的箭头函数不存在 this ,当计数器中不是箭头函数时 ,当 fn.call(this,...arg) 此时 fn() 的 this 与 计数器中 function() 的 this 同步 ,此时同计数器指向 Window ,所以为保证 fn() 的 this 指向 btn ,在 function() 中引入 that ,巧妙的越过了计数器中 function() 的 this 指向,使 fn() 的 that 同步 function() 的 that 指向 btn ,如下方代码所示:

 <script>
         let btn = document.getElementById('btn')
        function handle(e){
             console.log('向后端发请求')
        }
        btn.addEventListener('click',debounce(handle,1000))
        function debounce(fn,wait){
            var timer = null
        return function(...arg){
        let that = this  //使that指向btn
        clearTimeout(timer)
        timer = setTimeout(function(){
        fn.call(that,...arg) //使fn()的that与function()的that同步,指向btn
        },wait)
        }
        }
    </script>

四、防抖的胞弟:节流(Throttle)

面试官可能追加考题:"那节流呢?"——展现全面实力的时刻到了!

节流 : 有节制的执行:在一定时间内,只执行一次

代码如下:

<script>
        let btn =document.getElementById('btn')
        function handle(){
            console.log('向后端发生请求');   
        }
        btn.addEventListener('click',throttle(handle,2000))  //每两秒执行一次
        function throttle(fn,wait){
            let preTime = 0   //用闭包保留上一次执行的时间戳
         return function(...arg){
            let nowTime = Date.now()  //第一次点击的时间戳
            if(nowTime - preTime > wait){   
                preTime = nowTime
                fn.call(this,...arg)
            }
         }
        }
    </script>

总结:防抖在手,Offer我有!

防抖技术就像一位机智的交通警察:当疯狂点击的车流(高频事件)试图涌入服务器这座脆弱的小桥时,警察举起延迟执行的指挥棒喊道:"都给我排队!最后那辆车过,其他统统取消!"——闭包是它的执勤亭(藏定时器),参数传递是它的对讲机(精准通讯),this指向是它的警徽(锁定目标),而节流则是它的孪生兄弟,改成了"每分钟放行10辆车"的限流方案。掌握这套"事件交通管制系统",你就能让代码在面试官面前优雅通行,直通大厂收费站!