面试中常出现的考题 “防抖” 竟也没那么难

95 阅读7分钟

面试场景中,防抖衡量JS基础的核心考点

  • 考察闭包应用:理解timer变量如何通过闭包保存状态。
  • 检验异步编程:掌握setTimeoutclearTimeout的协作机制。
  • 区分能力:与节流(Throttle)对比体现候选人深度。

所以竟然防抖这么重要,那就让我们从以下几点来好好探讨下防抖有关的知识吧。

1. 什么是防抖?

定义:防抖是一种 通过延迟函数执行来优化高频事件触发 的编程技术。

主要体现为以下几点:

  • 当事件被连续触发时,函数不会立即执行,而是等待一个固定的 冷却延迟期(delay)
  • 若在延迟期内事件 再次被触发,则 重置延迟计时器
  • 只有 当事件停止触发且延迟期结束时,目标函数才会 被执行一次

2. 为什么需要防抖?

防抖的重要性

如果防抖不存在那可能出现什么问题呢?

  • 高频操作导致页面卡顿
  • 用户体验破坏(交互反馈混乱)
  • 资源浪费与逻辑错误。

场景:就比如当我们在一个页面输入了各种用户信息后点击提交按钮,但是因为网络延迟等原因,可能导致一段时间内没有响应,但是用户不知道问题所在可能会多次点击按钮,发送了许多网络请求。

后果:不仅带来了高并发,还可能因函数频繁执行导致卡顿,增加服务器压力,还可能导致服务器的崩溃等一系列问题。

所以这时我们就需要防抖来解决这些问题了。

3. 防抖的核心实现原理

在我们去实现防抖前先看下防抖所需要了解的一些关键技术和逻辑。

  • 关键技术点

    • 定时器 setTimeout:延迟执行目标函数。
    • 闭包:保存定时器变量,使其不被销毁。
  • 核心逻辑

    • 每次事件触发 → 清除旧定时器 → 设置新定时器。
    • 若在等待期内事件再次触发,则重置定时器。
    • 等待期结束无新触发 → 执行目标函数

4. 如何纯手写一串基础的防抖代码

首先我们用 js 获取按钮以至于后续我们方便追踪代码执行的情况

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">提交</button>

    <script>
        let btn = document.getElementById('btn')
        btn.addEventListener('click', function() {
            console.log('向后端发请求');
            
        })
    </script>
</body>
</html>

image.png

这里我们用浏览器打开可以看到如果用户因为什么原因导致向后端发送的请求而后端没有及时反馈导致用户习惯性的连续点击,向后端连续发生多个请求。如果同时出现大量这种情况无疑是对系统不利的,所以接下来我们就来解决这些问题。

在提交按钮在被一直点击时,可以设定一个时间延迟wait,当用户点击后等 wait 后再执行,当用户点击后没达到延迟时间 wait 时再次点击那就刷新让用户继续等一个 wait 时长再再执行代码,意思就是在没有新的触发时再执行。

以下为实现过程:

image.png

我们首先需要想的是怎么可以实现上上面描述的功能呢?答案是让函数体handle晚点触发就可以。所以我们就需要创建一个函数来实现这一功能。可以将在 btn.addEventListener('click',handle)handle改为传入一个函数debounce。因为btn.addEventListener('click',handle)中的handle是一个函数体,所以函数debounce也需要返回一个函数体:

image.png

可以借用官方定义的一个函数(定时器)来实现:

image.png

image.png

但是这时就会有问题了:当 wait 为 1000ms 时,假设我们用0.5秒点击了5下,那么将在1000ms 后连续发送5个网络请求,就类似于每点击一下就形成一个沙漏(每个沙漏上面沙子漏完的时间相同)。所以我们就要继续改进。每次点击就把上次点击形成的沙漏给销毁掉,这样就只会有一个沙漏将沙子漏完。所以就要用到另外一个官方定义的函数clearTimeout()了。

image.png

为什么要定义 timer= null 呢?因为需要形成闭包去保存 timer 的值,去实现每当有人点击按钮就会重新执行debounce这个函数,当 1000ms 内没点击,程序才会正常执行完(执行 fn())。

防抖虽然现在可以实现防抖,但是还有我们所需处理的细节:

参数问题

function函数被btn.addEventListener('click',debounce(handle, 1000))传入多个参数时就需要通过 ...arg 来完成这些参数的接收。而这些数据都是函数fn的,所以需要fn(...arg)fn全部接收。

function debounce(fn, wait){

        var timer = 0
        return function(...arg) {
            let that = this
            //console.log(e.x, e.y);
            clearTimeout(timer)         
            timer = setTimeout(function() {                   
            fn.call(that, ...arg)
            },wait)

### 然后就是this指向问题


首先我们来回顾 this 的指向:

#### this的绑定规则

1. 默认绑定 --- 当函数被独立调用时,函数的this指向window

2. 隐式绑定 --- 当函数引用上下文对象 且被该对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

3. 隐式丢失 --- 当一个函数被多层对象调用时,函数的this 指向最近的那一层对象

4. 显示绑定


- fn.call(obj, x, x, ...)

显示的将 fn 里面的 this 绑定到 obj 上, call负责帮 fn 接收参数


``` btn.addEventListener('click',debounce(handle, 1000))

所以我们只需将 this 指向 debounce(handle, 1000) 这里就等于 this 指向了 btn。

image.png

image.png

因为现在的 this 是独立调用,所以这是的 this 是指向 window 的,为了下面的 this

function handle(e){

        //console.log(e.x, e.y);
          console.log('向后端发出一个请求', this);
        // // 点击一次后就设置为禁用状态
        // btn.disabled = true
                    
    }

成功指向 fn.call(that, ...arg)中的 this 那就需要借助官方定义函数fn.call()来实现。而因为fn.call(that, ...arg)的 this 指向的是函数:function() {fn.call(that, ...arg)} 所以需要在这个function函数的同级定义一个let that = this来实现将this 指向函数:

image.png

以致于就实现了将下面的 this 指向了 btn ,到现在就完全完成了一个完整的防抖函数

function handle(e){

        //console.log(e.x, e.y);
        console.log('向后端发出一个请求', this);
        // // 点击一次后就设置为禁用状态
        // btn.disabled = true
                    
    }



```function debounce(fn, wait){
            var timer = 0
            return function(...arg) {
                let that = this
                //console.log(e.x, e.y);
                clearTimeout(timer)         
                timer = setTimeout(function() {                   
                fn.call(that, ...arg)
                },wait)
                // fn()
            }
        }

完整代码实现:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">提交</button> 
    <script>
        function handle(e){
            //console.log(e.x, e.y);
            
            console.log('向后端发出一个请求', this);
            // // 点击一次后就设置为禁用状态
            // btn.disabled = true
                        
        }
        let btn = document.getElementById('btn')

        btn.addEventListener('click',debounce(handle, 1000))
        
        function debounce(fn, wait){
            var timer = 0
            return function(...arg) {
                let that = this
                //console.log(e.x, e.y);
                clearTimeout(timer)         
                timer = setTimeout(function() {                   
                fn.call(that, ...arg)
                },wait)
                // fn()
            }
        }

    </script>
</body>
</html>

5. 防抖 vs 节流

有些同学可能会搞混防抖和节流,起初我也是这两个傻傻分不清,在专门了解后发现区别很明显,并且一点也不复杂。那么防抖和节流到底有哪些区别呢?看完一张图你就会完全知道他们的区别了

image.png

节流(参数问题和this指向都是一样的)

  • 有节制的执行:在一定的时间内,只执行一次
  • 考虑参数问题
  • 考虑 this 指向

节流只需用系统自带函数Date.now()实时时间,然后用nowTime - preTime > wait的关系式即可判断是否执行代码。

代码实现:


<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Document</title>

</head>

<body>

    <button id = 'btn'>确认</button>

  


    <script>

        let btn = document.getElementById('btn')

        function handle() {

        console.log('向后端发请求', this);

           

        }

        btn.addEventListener('click', throttle(handle, 2000))

        function throttle (fn, wait) {

            let preTime = 0

            return function(...arg) {

                let nowTime = Date.now()  //  第一次点击的时间戳

                if(nowTime - preTime > wait) {

                    fn.call(this, ...arg)

                    preTime = nowTime  

                }

                //console.log(nowTime);              

                //fn()

            }

        }

    </script>

  


</body>

</html>

6. 注意事项

  • this 指向问题:使用 func.apply(this, args) 确保上下文正确。
  • 参数传递:返回函数用 ...args 接收事件参数。
  • 内存泄漏:组件销毁时清除定时器(如 React useEffect 的清理函数)。

7. 总结

在我们学习防抖时必须要完全了解每一部分代码的原由,这些代码怎么来的,为什么要这么写。在搞明白这些后你就可以自己根据那些逻辑去写出一串完整的防抖代码,在面试的时候无论是手写防抖代码还是面试官问你有关问题就都可以顺利解答了。