JS 防抖

大家好,今天我们来讲一下JS之防抖相关的内容,节流防抖的相关运用是前端学习框架中一个较为关键的部分,也是js闭包的经典案例。在学习内容之前,先来看一下我们今天所要掌握的一些知识点:

  • 什么是防抖?防抖的概念?
  • 为什么需要防抖?
  • 怎么样去实现防抖?
  • 防抖的几个关键点
  • 总结

一 概念

防抖:防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

二 为什么需要防抖

简单来说,防抖就是为了优化资源提升性能,我们先从防抖这个概念入手。例如我们同一时间点击鼠标,从客户端要向服务器发送大量的同质化的内容,那服务器势必会承受很大压力,那怎样对大量的一些相同的内容进行某些操作从而提升服务器的性能呢?

我们现在要做一个小案例,案例是这样的,当我们鼠标在紫色盒子内快速移动时,盒子内的数字不会发生变化,而当我们鼠标停顿一小会时,盒子内的数字在原有的值上+1

这里的css以及html代码先给大家,可自行拷贝

    * {
        margin: 0;
        padding: 0;
    }

    div {
        width: 150px;
        height: 150px;
        background-color: rgb(249, 156, 34);
        text-align: center;
        line-height: 150px;
        margin: 100px auto;
        border-radius: 20px;
        filter: hue-rotate(240deg);
        -webkit-box-reflect: below 1px linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.7));
        font-size: 26px;
        color: #fff;
    }
    
    
        <div>0</div>

三 代码实现

首先,我们在script模块中获取到这个div盒子,给盒子绑定一个mousemove事件,这个事件就是移动,如图

JS 防抖

这时我们就获取到了盒子,并在它身上绑定了一个mousemove移动事件。关于括号里面的debounce函数是什么意思我们接着往下讲解:

大家都知道一个函数名后面如果有括号就代表执行的意思,例如

function debounce() { console.log(123) } debounce()

JS 防抖

因此函数名+() ,debounce()就是立即调用。而在div的移动事件中,由于我们直接调用了debounce(…),所以无论我们是否触发mousemove事件,debounce函数早已执行里面的部分。那么我们debounce函数体内到底有什么内容呢?如代码区域

      // 回调函数
    function debounce(x, y) {

        let timer = null


        return function () {

        }
    }

我们在debounce函数体内声明了一个timer,赋值为null,再往下执行是返回了一个子函数function,注意,我们仅仅只做了return返回这个子函数体操作。因此,如果把div事件中的回调函数换一种写法,如图

JS 防抖

可以看到,上面与现在结果是完全相同的,那么我们为什么一开始大费周章的写debounce()呢?在本文临近末尾处进行解释。

四 总体思路

我们实现要防抖效果,当鼠标在盒子内移动时我们就要不断地监测这个触发事件,如果发现事件并没执行完毕,那就不显示效果。一旦监测到触发事件完成的条件,就显示效果。

逻辑代码实现思路:我们可设置一个倒计时函数,在倒计时函数内写入我们真正要执行的事件,也就是要实现的效果,可以是一个函数。当我们检测到事件被不断触发,那我们就不断的清空上一个定时器,开启下一个定时器,执行里面的事件

return中的setTimeout()倒计时代码区域如下:

         return function () {

            clearTimeout(timer)

            timer = setTimeout(() => {

            })
        }
        

因为我们是要倒计时函数内执行事件,所以我们不妨重新定义一个函数,这个函数内包含我们要执行的事件,事件是div的值递增。又因为我们要让盒子内的数字不断递增,所以我们要声明一个数字并设置其一个初始值,代码如下

   // 声明变量赋值
    let i = 1

    // 回调函数中定时器内部的事件
    function fn() {
        // 实现div盒子现实的值递增
        div.innerHTML = i++

        // 此时打印this,,指向的是div
        console.log(this);
    }

    

我们把这个fn函数添加定时器内,满足条件时定时器就触发这个函数

        return function () {

            clearTimeout(timer)

            timer = setTimeout(() => {
                fn()
            })
        }

但现在我们发现这个定时器没有设置毫秒数,所以可以在一开始的div的绑定事件的回调函数中,传递一个参数,最后落到这个定时器函数内,如图:

JS 防抖

这样我们就完成了时间参数的传递。另外我们想一想这个fn()事件函数能不能也作为一个参数传递给定时器内部呢?上文已提到过,是可以的。于是

JS 防抖

这样一来,我们就基本完成了代码的书写,打开浏览器也能得到相应的效果。但现在有一个小问题,我们需要完善它,就是改变fn()事件内部的this指向。如果我们什么都不改,打印fn内部的this,控制台的效果是这样的

JS 防抖

可以看到fn内部的this指向的是全局的window,那它既然作为我们真正的回到函数中要执行的部分,我们肯定要修改它里面的this指向,因此我们可以选择call(),apply(),bind()三种方法来实现这个需求,这里我们就用call,如图:

JS 防抖

因为在箭头函数内部用到this我们需要考虑到上下文**<箭头函数没有内置的this>**,所以在这个地方,箭头函数的this直接继承父级函数function中的this,而这个function函数作为真正的回调函数,其this指向事件对象div,所以箭头函数中的this指向的也是div,所以参数(fn)改变指向,从指向windows到指向div。这样一来,我们就完成了最初的需求。再回到前文中的一个问题,

JS 防抖

红色框框里面为什么要这样写?原来,这是为了方便传递参数给真正回调函数,在实际开发中我们有太多的不同事件要执行,如果写死,不利于程序的可读性,也可能会降低开发的效率。

五 防抖注意事项

函数名+()会执行函数,返回结果,但并不是所有的fn()都会执行,例如

JS 防抖

控制台不会打印gn函数内部的结果,因为我们在最外面没有调用fn函数!

闭包的实现

何为闭包?闭包是内部函数访问外部函数变量的集合

JS 防抖

为何timer不能声明在真正的回调函数的内部?

JS 防抖

因为timer如果声明在真正的回调函数的内部,那我们每次触发鼠标移动事件,函数内部都会创建一个值为null的变量,下面的清除操作相当于完全不存在,因为清除不了前一次的timer定时器里面,页面上不能实现真正的效果,大家可试一试

改变函数内部this的指向

改变函数内部this的指向的方法有很多,例如

JS 防抖

那么在后面的运行中,gn函数内部的this就会指向外部fn函数的this。除此之外,call(),apply(),bind()等方法都可以改变函数内部this的指向。

call(),apply(),bind()区别

  • call()接受参数列表
  • apply()接受数组
  • bind()返回一个新的函数,可以声明变量接收

总结

这节课我们完成了防抖案例的基本实现,需要我们深刻理解函数内部的逻辑执行顺序以及有关概念,在此基础之上能正确的处理学习当中有关防抖的问题。这次的分享就到这里,谢谢大家

script区域全部代码:

    // 获取元素
    const div = document.querySelector('div')
    
   // 声明变量赋值
     let i = 1
    
    // 绑定事件
    div.addEventListener('mousemove', debounce(fn, 300))

    // 回调函数
    function debounce(x, y) {
        // 设置定时器
        let timer = null

        // 返回真正的回调函数function
        return function () {

            // 关闭上一次开启的定时器
            clearTimeout(timer)

            // 开启这次的定时器
            timer = setTimeout(() => {
                // 执行事件
                x.call(this)
            }, y)
        }
    }
 

    // 回调函数中定时器内部的事件
    function fn() {
        // 实现div盒子现实的值递增
        div.innerHTML = i++

        // 此时打印this,,指向的是div
        console.log(this);
    }