JS性能优化之 防抖

2,555 阅读4分钟

前言

  在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但是有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数,这会加重浏览器的负担,导致用户体验非常糟糕。因此我们可以采用debounce(防抖)的方式来解决这个问题,使用户体验更佳。

什么是防抖

  举个栗子,坐电梯的时候,如果电梯检测到有人进来(触发事件),就会多等待 10 秒,此时如果又有人进来(10秒之内重复触发事件),那么电梯就会再多等待 10 秒。直到10秒内电梯检测到没有人进来,就关闭电梯开始运行。因此,“防抖”的关键在于,一个事件发生一定时间之后,才再次触发事件。

为什么要防抖

  • 先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。简单的一个触发事件,绑定输入事件向后端发送请求。

    html代码如下

  <input type="text" id="search">
  <script>
      const search = document.getElementById('search');
      function handlesearch(event) {
          console.log( event.target.value);
      }
      search.addEventListener('input',handlesearch);//绑定事件
  </script>

在上述代码中,input 元素绑定了 handlesearch事件,当在输入框连续输入内容时,会持续地去触发该事件。效果如下

可以看到,我们每输入一个字母就触发了一次事件,如输入“jay”就要触发3个请求。用户连续输入就会连续触发input, 但是用户处于输入中,显然是没必要请求的,接下来看看防抖是怎么解决这个问题的。

  • 解决:等用户输入完毕再去请求 => “防抖”,代码如下
<input type="text" id="search">
<script>
    const search = document.getElementById('search');
    let timeout;                                 //定时器
    function handlesearch(event) {
        if(timeout) {
            clearTimeout(timeout);               //清除之前的定时器,重新开始计时
        }
        timeout = setTimeout (() => {            //启动定时器
            console.log( event.target.value);    //发请求
        },600)                                   //等待时间600ms
    }
    search.addEventListener('input',handlesearch);//绑定事件
</script>

上述防抖代码中,使用一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。效果如下:

显然,用户一输入 input并没有被立即触发,而是等了600ms之后输入完成,才开始请求。

到这里,已经把防抖实现了,现在给出定义:

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

多个事件防抖怎么封装

  很显然,以上是针对单独事件的防抖,那多个事件呢,或许你会说,那给每个事件都添加防抖功能不就行了,的确可以,但是我们可以优化,统一处理,让其抖功能进行封装。那怎么做呢?

  • 直接使用别人设计的防抖 api。

    我们可以参考Lodash网站里面封装好的 debounce(防抖)方法,这是别人写好的第三方库,可以直接拿来用,

    附:

    Lodash网站: www.lodashjs.com/docs/lodash…

    Bootcdn: www.bootcdn.cn/

    首先要在Bootcdn引入三方库,然后直接贴cdn地址:

    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
    

    代码如下

    <input type="text" id="search1">
    <input type="text" id="search2">
    <input type="text" id="search3">
    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
    <script >
       // 给search1添加防抖功能
       const search1 = document.getElementById('search1');
       search1.addEventListener('input',_.debounce( function (e)  {
           console.log(e.target.value);
       },600))
    </script>
    

    直接引入_.debounce实现了防抖,效果如下:

  • 模仿 防抖 封装 优化

当我们有多个输入框都要有防抖功能时,该怎么做?

接下来我们自己来写一个有防抖功能的函数,封装在debounce内部,方便复用。

<script>
    const search1 = document.getElementById('search1');
    search1.addEventListener('input',debounce( function (e)  {
        console.log(this);
        console.log(e.target.value);
    },600))
    
    function debounce(func, wait) {
        let timeout;
        //写一个具有防抖功能的自己内部定义的函数function
        return function(event) {
            if(timeout) {
               clearTimeout(timeout);
            }
            timeout = setTimeout (() => {              //定时器
               func.call(this, event)
            },wait)
        }
    }
</script>

这是我们参考Lodash里面的自己写出来的防抖功能,实现了函数调用,功能依然可以实现。

思想:接收一个没有防抖功能的函数func,返回一个有防抖功能的函数function,在里面设置一个定时器timeout。写的代码跟上面的例子都差不多,但是这里是封装成了一个函数,在多个事件需要防抖时,就能简化代码了。

手写debounce

	//实现防抖 步骤
    //1.let timeout
    //2. if(timeout)  clear
    //3. timeout = setTime
    function debounce(fn, wait) {
        let timeout = null;
        return function(event) {
            if(timeout) clearTimeout(timeout);
            timeout = setTimeout(() => {
                fn.call(this,event);
            },wait)
        }
    }

手写throttle

	//函数节流:使得一定时间内只触发一次函数。
    //原理是: 通过判断是否到达一定时间来触发函数。
    //函数节流主要有两种实现方法:时间戳和定时器
    
    //以下是两种结合
    function throttle(func, wait){
        var previous = 0;
        var timeout = null;
        return function(event) {
            var now = new Date().getTime();
           let remain = wait - (now - previous);
           //时间戳
           if(now - previous > wait) {
                func(); //执行函数
                previous = now; //更新时间戳
            }
            //定时器
            else if(!timeout){
                timeout = setTimeout(() => {
                    func(); //执行函数
                    timeout = null;//清空定时器
                    previous = new.Date().getTime(); //更新previous
                },remain)
           }
        }
    }

总结

  • debounce使用场景
    1. keyup事件搜索框,只需用户最后一次输入完,再发送请求
    2. ousemove事件,鼠标移动拖拽
    3. resize事件,浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。
    4. scroll事件,资源的加载
  • debounce(防抖)和throttle(节流)一般都会写在一起讲述,这里没有细讲节流,想了解可以参考这里很详细github.com/mqyqingfeng…

不足之处,请多指教,感谢阅读!