保姆级教学:彻底教会你手搓防抖,解决闭包this和事件参数问题

183 阅读6分钟

前言

当我们在浏览网页信息,在搜索栏搜索我们需要的信息时,我们时常会出现卡顿的现象。作为网络攻城狮的我们肯定知道是因为发送的请求还在路上,或者后端还未解析完成。但很大一部分人的做法是--反复点击搜索键,于是糟糕的事情发生了,当你一直点击时你会一直给其后端服务器发送请求。那么问题来了,如果存在这么一个情况----数亿人在浏览这个网页时都出现网络出现卡顿的情况,他们都选择了不断点击重复发送请求,那么服务器将会可能出现瘫痪的情况。如何解决这个问题?这就是我们这篇文章学习的重点--防抖

  • 注意:基础较弱小伙伴可以移步以下篇章

juejin.cn/post/736757…

juejin.cn/post/736901…

juejin.cn/post/736901…

防抖

怎么解决用户一直点一直发送请求的方式呢?只要实现----当用户一直点击按钮时我们不发送请求,但用户冷静几秒后再点击按钮时可以发送请求。和时间相关肯定需要利用定时器来操作。

主要步骤

让我来梳理一下主要步骤:

  1. 先搓个按钮
  2. 当用户点击按钮时发送请求,所以需要监听鼠标的点击事件
  3. 设置计时器,当用户在点击按钮的1s之内再次点击按钮时计时器清零

实操步骤

  1. 手搓一个按钮先
 <button id="btn">提交</button>
  1. 给这个按钮绑定点击事件
  • 拿到btn的DOM结构,使得我们能对变量btn进行操作
  • 给 btn 这个 DOM 元素添加一个点击事件监听器
let btn = document.getElementById('btn')

btn.addEventListener('click', function(){});

3.在监听器中设置计时器,记录用户点击后经过的时间

我们把监听器中形式功能的函数拿到外面来写,设置点击后1s发送请求

 setTimeout(
            function () {
                console.log('提交')
            },
            1000
        )

单纯的发送请求并不能达到我们的想要的结果,我们的目的无非就两个: 闭环当点击按钮时触发定时器

  • 当在1s内点击按钮时毁灭这个定时器,重新创造一个定时器计时

闭包循环

所以,我们需要使用闭包来达成一个循环,将执行操作的函数放在我们打造的防抖函数里,形成闭包。

  • 我们定义一个函数debounce来作为我们的防抖函数,当全局执行完成时他必须被调用
  • 在一开始时设定timer来控制计时器的有无
  • 在防抖函数的内部设置一个函数,这个函数需要返回出来,遵守addEventListener的语法
  • 在这个返回函数中放置清除计时器clearTimeout()和计时器 setTimeout()
  • 当这个计时器生效时发送‘提交’信号

代码实现如下:

   let btn = document.getElementById('btn');

   btn.addEventListener('click', debounce());


        //防抖函数
        function debounce() {
            let timer = null;

            return function () {
                //如果第二次的时间没到一秒,就销毁上一个定时器
                clearTimeout(timer)
                timer = setTimeout(

                    function () {
                        console.log('提交')
                    }
                    , 1000)
            }

        }

我们可以优化一下我们的代码:

 let btn = document.getElementById('btn')

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

        function debounce(fn) {

            let timer = null

            return function () {

                clearTimeout(timer)
                timer = setTimeout(fn, 1000)

            }
        }

        function handle() {
            console.log('提交')
        }

这样的话,当鼠标第一次点击时,变量timer会变成一个计时器;当在1s时间内(计时器未工作完成时)再次点击按钮会再次执行防抖函数的内置函数,此时会把上一个未完成工作的计时器销毁,只有等上一个计时器工作完成后才会重新创造一个计时器,重新倒计时发射信号。

闭包工作原理如图: Snipaste_2024-05-28_12-26-34.jpg

服务器瘫痪的情况出现过多次,著名的新浪微博,b站都出现过这种情况。当服务器出现这种情况时,我们程序猿不得不从百忙之中又抽身跑回公司疯狂修改bug,防抖的实现真是程序猿的福音。但是,难道防抖的实现到这里就结束了吗?不不,我们实现了防抖的同时还带来了一点点小问题。

防抖中的this指向问题

我们就以这段代码为例

 let btn = document.getElementById('btn')

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

        function debounce(fn) {

            let timer = null

            return function () {

                clearTimeout(timer)
                timer = setTimeout(fn, 1000)

            }
        }

        function handle() {
            console.log('提交'this)
        }

首先一个问题handle里面的this指向谁?显而易见,他指向了window,但我们在进行防抖操作之前,这里的this是指向btn的,我们改变了this的指向,就必须将他掰正。

还记的显示绑定的三个方法吗?call(),apply(),bind(),在这里我们只需要将handle里的this绑定到return的那个函数上就行,因为他的this指向是btn。

除此之外,我们可以将定时器内函数改为箭头函数,利用箭头函数没有this,箭头函数内的this是包含他的函数的this这一概念,将this掰到你需要绑定到的this上加以修正

let btn = document.getElementById('btn')

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

function debounce(fn) {

    let timer = null

    return function () {

        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.call(this)
        }, 1000)
    }
}

function handle() {
    console.log('提交')
}

那计时器里面不是箭头函数该怎么解决呢?

   timer = setTimeout(
            function () {
               
            }, 1000
        )}
}

在这里我们就不能再将this绑定到外部函数的this了,因为自己含有this。但是我们可以定义外部函数的this再进行绑定。在这里我们可以将return函数的this重新赋值为that

return function () {
        const that = this
        clearTimeout(timer);
        timer = setTimeout(
            function () {
                fn.apply(that)
            }, 1000
        )
    }

修正事件参数

任何一个事件在被触发的时候都会有事件参数,这是引擎内置的,让我们更好的理解代码。在我们做防抖之前,handle函数里面有一个事件参数e他是指向click的

btn.addEventListener('click', function(){});

function handle(e) {
    console.log('提交',e)
}

微信图片_20240528135629.jpg

但是当我们防抖后改变了事件参数e,变成了undefined ,这是因为handle函数变成了debounce的内置,事件参数指向匿名函数。我们需要用call重新绑定

        function debounce(fn) {
            let timer = null;
         
            return function(e) {
                const that = this;
                
                // 如果第二次的时间没到1s,就销毁上一次的定时器
               clearTimeout(timer);
               timer = setTimeout(function() {
                fn.call(that, e);
               }, 1000);
               
            }
        }

完美~

小结

如何手搓一个防抖?

  1. defounce 返回一个函数体,跟debounce形成闭包
  2. 子函数体中每次先销毁上一个定时器再创建一个新的setTimeout
  3. 还原 原函数的this指向
  4. 还原 原函数的参数

这篇文章理解吃力的小伙伴还请移步以下篇章:

juejin.cn/post/736757…

juejin.cn/post/736901…

juejin.cn/post/736901…

理解以上篇章内容再次移步想必会有事半功倍的效果