还在被重复提交困扰?手把手教你写出高质量防抖函数!

188 阅读5分钟

"你是否在生活中遇到过输入框每敲一个字就发送一次请求, 相关搜索栏就刷新一次, 引起页面卡顿呢?

"你是否也在双十一又或者618购物大节,卡点抢购商品,疯狂点击购买而页面卡住,显示页面繁忙,请重试呢?”

本文带你揭秘是如何解决这些问题, 让我们一起走进"防抖的大门", 探索防抖的具体实现原理。


一、 防抖的应用场景

你有没有遇到过这样的尴尬场景: 当你在一个表单辛辛苦苦填完所有信息, 点击"提交"按钮后, 页面却迟迟没有响应。你以为是自己没有点到, 又或者以为网络问题---网卡了, 于是又点了一次、两次......结果后台会因为多次点击按钮, 带来了高并发, 极大的增加了服务器的压力, 当压力过大甚至有可能引起服务器的崩溃。为了应对这种应用场景, “防抖”(debounce)技术因此而生。


二、 那到底什么是防抖呢?

为了应对多次点击按钮的恶劣情况, 实际上还有一个更简单的方法实现, 那就是在按钮点击后立即禁用按钮, 可以有效防止多次点击, 但是绝大数应用场景是不适应这种方式的, 比如说一些音乐APP的搜索框, 你不能只让它允许搜索一次吧。

于是防抖技术便是对此方法的升级, 通俗点来说, 防抖就是让某个操作在短时间内只执行最后一次。比如你连续点击了好几次提交按钮, 防抖会让"等你点完, 停下来"后, 只让最后那一下提交真正生效。 这样就能有效避免因为用户手快或者网络卡顿等原因导致的重复的提交、频繁请求等问题。

和防抖经常一起出现的"节流"(throttle)不太一样, 节流是每隔一段时间最多"执行一次", 而防抖则只是"在操作结束后执行一次"。两者虽然名字差不多, 但用法和场景大不相同。


三、 防抖的原理图解

其实, 防抖的核心原理很简单: 每次触发事件时, 都重新计时, 只有等你"停下来"一段时间后, 才开始真正执行操作。 我们来看一段最基础的防抖函数代码:

function debounce(fn, wait) {
    let timer = null
    return function() {
        clearTimeout(timer); // 每次触发都清楚上一次的定时器
        timer = setTimeout(() => {
            fn() // 只在最后一次触发时执行
        }, wait)
    }
}
  • 用户每点击一次按钮, debounce函数就会清楚上一次的定时器, 并重新开始计时。
  • 如果用户一直点击, 定时器就会一直重置, 真正的操作(比如提交表单)始终不会被执行。
  • 只有当用户停下来时, 超过设定的wait时间, 定时器到点, 执行里面操作, fn才会真正被调用。

image.png

"如果所示, 每次触发事件, 上一次的定时器都会被清楚, 只有最后一次触发后, 才会真正执行操作。"


四、 手写实现, 从零开始打造属于你的防抖函数

1. 最基础的防抖实现

以下是最简单的防抖函数, 只实现"延迟执行"

function debounce(fn, wait) {
    let timer = null // 用于保存定时器
    return function() {
        clearTimeout(timer) // 每次触发都会清除上一次的定时器
        timer = setTimeout(() => {
            fn()
        }, wait)
    }
}

用法示例

let btn = document.getElementById('btn')

function handle() {
    console.log("向后端发送请求")
}

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

2.支持参数传递和解决this指向问题

  • 上面的实现有个问题: 如果fn需要参数, debounce里没法传递。我们可以用...args来解决
  • 如果fn内部用到了this, 上面的实现会丢失原本的this, 匿名函数的this将会指向btn, 箭头函数继承外部的this, 而fn将会丢失this指向, 在浏览器的引擎下, 此时this的指向会是window 为解决上述问题, 代码需要进行改进, 具体实现如下:
function debounce(fn, wait) {
    let timer = null // 用于保存定时器
    return function(...args) 
        clearTimeout(timer) // 每次触发都会清除上一次的定时器
        timer = setTimeout(() => { // 重新设定定时器, wait毫秒后执行fn
            fn.call(this, ...args) // 用call保证fn内部的 this 和 参数不变 
        }, wait)
    }
}

3. 用法小结

这样写出来的防抖函数, 既能传递参数, 也能保证this指向正确, 适用于绝大数实际开发场景。


五、 实战案例

接下来我将用一个简单的案例来演示如何用防抖优化体验

1. HTML结构

<button id="btn">提交</button>

2. JS代码集成防抖

let btn = document.getElementById('btn')

        function handle(e) {
            console.log(e.x, e.y); // 用来测试参数是否接收成功
            console.log('向后端发送请求', this)
        }
        btn.addEventListener('click', debounce(handle, 1000))

        function debounce(fn, wait) {
            var timer = null
            return function(...arg) {
                let that = this
                clearTimeout(timer) 
                timer = setTimeout(function() {
                    fn.call(that, ...arg)
                }, wait)
            }
        }

3. 运行效果

用户疯狂点击按钮时, 只有停下来1000ms后, 才会真正触发handle函数

ezgif-1bc43efdc4e009.gif


六、 扩展: 防抖和节流的区别

1. 节流是什么?

节流的核心思想: 无论你多频繁触发事件, 一定时间内只会执行一次 比如你在页面上疯狂滚动, 节流可以让滚动事件每隔100ms才触发一次, 避免页面卡顿

2. 防抖 vs 节流

  • 防抖: 只在操作结束后执行一次(如输入框搜索、按钮防重复提交)
  • 节流: 每隔一段时间执行一次(如页面滚动、窗口resize、鼠标拖曳)

3. 节流的简单实现

function throttle(fn, wait) {
    let preTime = 0
    return function(...args) {
        let nowTime = Date.now()
        if (nowTime - preTime > wait) {
            fn.call(this, ...args)
            preTime = nowTime
        }
    }
}

七、 总结

防抖和节流是前端开发中提升用户体验和优化性能的两大利器。防抖能有效避免重复提交、频繁请求等问题,让页面更流畅、服务器更轻松;节流则适合高频率事件的限流处理。掌握它们的原理和实现,不仅能让你的代码更优雅,也能在面试和实际项目中游刃有余。希望本文的讲解和案例,能帮你彻底搞懂防抖,从容应对各种高频触发场景!