JS函数防抖和节流全解析

 

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。

html 文件中代码如下

<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
    let num = 1;
    let content = document.getElementById('content');

    function count() {
        content.innerHTML = num++;
    };
    content.onmousemove = count;
</script>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下

可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。也就说当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定时间到来之前,又触发了事件,就重新开始延时。也就是说当一个用户一直触发这个函数,且每次触发函数的间隔小于既定时间,那么防抖的情况下只会执行一次。——函数防抖只是在最后一次触发事件后才触发一次函数

防抖的原理就是:你尽管触发事件,但是我一定会在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!

防抖函数分为非立即执行版和立即执行版。

非立即执行版:

function debounce(func, wait) {
    let timeout; // 定义一个定时器
    return function () {
        let context = this;
        let args = arguments;
        
        // timeout声明未赋值为undefined。undefined作为if判断条件时会被解释为false     
        if (timeout) clearTimeout(timeout);// 清除这个定时器
        
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
}

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,我们可以这么使用

content.onmousemove = debounce(count,1000);

完整代码如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
      let num = 1;
      let content = document.getElementById("content");
      // 此处为高频调用函数
      function count() {
        content.innerHTML = num++;
      }
      content.onmousemove = count;

     
      function debounce(func, wait) {
        let timeout;
        return function () {
          let context = this;
          // console.log(this)
          let args = arguments;

          if (timeout) clearTimeout(timeout);

          timeout = setTimeout(() => {
            func.apply(context, args);
          }, wait);
        };
      }
      content.onmousemove = debounce(count,1000);
    </script>
  </body>
</html>

效果如下

可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。

上述防抖函数的代码还需要注意的是 this 和 参数的传递

let context = this;
let args = arguments;

防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。

立即执行版:

function debounce(func,wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);

        let callNow = !timeout;
        timeout = setTimeout(() => {
            timeout = null;
        }, wait)

        if (callNow) func.apply(context, args)
    }
}

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。使用方法同上。

完整版代码如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
            let num = 1;
            let content = document.getElementById("content");

            function count() {
              content.innerHTML = num++;
            }
            content.onmousemove = count;


            function debounce(func, wait) {
              let timeout;
              return function () {
                let context = this;
                let args = arguments;

                if (timeout) clearTimeout(timeout);

                let callNow = !timeout;
                timeout = setTimeout(() => {
                  timeout = null;
                }, wait);

                if (callNow) func.apply(context, args);
              };
            }
            content.onmousemove = debounce(count,1000);
    </script>
  </body>
</html>

运行效果如下

在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。
 

双剑合璧版:

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
function debounce(func,wait,immediate) {
    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

完整示例代码如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
      let num = 1;
      let content = document.getElementById("content");

      function count() {
        content.innerHTML = num++;
      }
      content.onmousemove = count;

      /**
       * @desc 函数防抖
       * @param func 函数
       * @param wait 延迟执行毫秒数
       * @param immediate true 表立即执行,false 表非立即执行
       */
      function debounce(func, wait, immediate) {
        let timeout;

        return function () {
          let context = this;
          let args = arguments;

          if (timeout) clearTimeout(timeout);
          if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
              timeout = null;
            }, wait);
            if (callNow) func.apply(context, args);
          } else {
            timeout = setTimeout(function () {
              func.apply(context, args);
            }, wait);
          }
        };
      }
      content.onmousemove = debounce(count, 2000, false);
    </script>
  </body>
</html>

运行效果如下 

👨‍🌾 分析:参数immediate设置的是false值,为true 执行的是立即执行版,为false 执行的是非立即执行版。元素对象content的onmousemove事件,鼠标移动一次,间隔2秒后,执行一次非立即执行版的定时器函数,如果在间隔时间(2秒)内,再次或是频繁多次移动鼠标,则不会执行定时器函数(即数字不会增加)

👨‍🌾 小结:

JS防抖其实也是用来提升网页性能的一种技术写法

防抖就是指当用户触发某个操作时,如果在指定的时间内再次触发该操作,那么就清除前面触发的操作(即清除掉上一次的定时器),直到用户操作之后并且在指定的时间内不再操作我们再处理用户的请求。


常见的场景:如用户在输入框输入内容,我们根据用户输入内容去查数据。

当用户输入文字时,监听input事件,那么用户每输入一个字符都会触发查询,这样就会发起很多个请求

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="myInput" />

    <script>
      let input = document.querySelector("#myInput");
      function getInputValue(){
        console.log(input.value)
      }
      input.addEventListener('input',getInputValue)
    </script>
  </body>
</html>

运行结果:

所以,我们可以考虑一下,当用户输入关键字的时候并且在3秒内不再输入文字,我们才发请求,否则我们就什么都不做
 

所以我们应该每次都使用一个定时器来保存用户的操作,然后每次在指定时间内触发输入的时候都把上一次的定时器清除掉即可,这样就保证了只发一次请求

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="myInput" />

    <script>
      let timer = null;
      let input = document.querySelector("#myInput");
      function getInputValue() {
        if (timer) {
          clearTimeout(timer);
        }
        timer = setTimeout(() => {
          console.log(input.value);
        }, 1000);
      }
      input.addEventListener("input", getInputValue);
    </script>
  </body>
</html>

但是直接把定时器设置为全局变量容易导致变量在别处被修改,所以我们可以用函数封装一下定时器变量,方便给需要的函数调用。——闭包的一个场景就是函数防抖节流。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="myInput" />

    <script>
      let input = document.querySelector("#myInput");
      function getInputValue() {
        console.log(input.value);
      }

      function debounce(fun, delay) {
        let timer = null;
        return () => {
          if (timer) {
            clearTimeout(timer);
          }
          timer = setTimeout(() => {
            fun();
          }, delay);
        };
      }
      input.addEventListener("input", debounce(getInputValue, 3000));
    </script>
  </body>
</html>

运行结果:

在debounce里面定义变量存储定时器,并且我们可以设置fun,delay来接收外部传来的函数以及延迟的时间,然后我们返回一个函数即可。

这样,当我们使用addEventListener('input',debounce(fn,delay))绑定事件时,debounce就会立即执行,所以实际input事件绑定的是return的那个函数

这样,当每次触发input时都会执行对应的函数,从而实现节省性能,实现只发最后一次请求的结果了


节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。所以,节流会稀释函数的执行频率。也就是说,节流就是当持续触发事件时,保证在一定时间内只调用一次事件处理函数,意思就是说,假设一个用户一直触发这个函数,且每次触发小于既定值,函数节流会每隔这个时间调用一次。——函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数

用一句话总结防抖和节流的区别:防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行。防抖和节流都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。


如图所示,防抖和节流之间的区别:


可以看出, 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:

这样的方式会触发第一次,而不会触发最后一次。

使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行

function throttle(func, wait) {
    // 首先获取使用节流机制时的时间
    //let previous = Date.now(); 
    // 也可以赋值为0
    let previous = 0;
    return function() {
        // 再获取调用时的时间
        let now = Date.now();
        let context = this;
        let args = arguments;
        // 若两个时间差超过了设置的时间,调用函数
        if (now - previous > wait) {
            // 立即执行
            func.apply(context, args);
            previous = now;
        }
    }
}

使用方式如下 

content.onmousemove = throttle(count,1000);

完整示例代码如下

​
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
      let num = 1;
      let content = document.getElementById("content");

      function count() {
        content.innerHTML = num++;
      }
      content.onmousemove = count;

      function throttle(func, wait) {
        // 首先获取使用节流机制时的时间
        // let previous = Date.now();
        let previous = 0;
        return function () {
          // 再获取调用时的时间
          let now = Date.now();
          let context = this;
          let args = arguments;
          // 若两个时间差超过了设置的时间,调用函数
          if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
          }
        };
      }
      content.onmousemove = throttle(count, 1000);
    </script>
  </body>
</html>

效果如下

可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

定时器版:

这样的方式会触发最后一次,而不会触发第一次。

使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行

function throttle(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        // 设置一个定时器,只有为空时才会触发,每次执行后都会重新设定一个定时器。
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}

使用方式同上

完整示例代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
      let num = 1;
      let content = document.getElementById("content");

      function count() {
        content.innerHTML = num++;
      }
      content.onmousemove = count;

      function throttle(func, wait) {
        let timeout;
        return function() {
            let context = this;
            let args = arguments;
            // 设置一个定时器,只有为空时才会触发,每次执行后都会重新设定一个定时器。
            if (!timeout) {
              timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
              }, wait)
            } 
          }
        }
        content.onmousemove = throttle(count, 1000);
    </script>
  </body>
</html>

效果如下

可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。

我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。所以说,时间戳版会触发第一次,而不会触发最后一次。定时器版会触发最后一次,而不会触发第一次。

同样地,我们也可以将时间戳版和定时器版写法的节流函数结合起来,实现双剑合璧版的更加精确的节流函数。

双剑合璧版:

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func, wait ,type) {
    // 此处定义的两个变量只会在第一次调用throttle方法时生效,之后都是执行闭包返回的函数
    let previous = 0;
    let timeout ;
    return function() {
        let context = this;
        let args = arguments;
        if(type===1){
            let now = Date.now();

            if (now - previous > wait) {
                //时间到了执行
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

完整示例代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div
      id="content"
      style="
        height: 150px;
        line-height: 150px;
        text-align: center;
        color: #fff;
        background-color: #ccc;
        font-size: 80px;
      "
    ></div>
    <script>
      /**
       * @desc 函数节流
       * @param func 函数
       * @param wait 延迟执行毫秒数
       * @param type 1 表时间戳版,2 表定时器版
       */
      let num = 1;
      let content = document.getElementById("content");

      function count() {
        content.innerHTML = num++;
      }
      content.onmousemove = count;


      function throttle(func, wait, type) {
        let previous = 0;
        let timeout ;
        return function () {
          let context = this;
          let args = arguments;
          if (type === 1) {
            let now = Date.now();
            if (now - previous > wait) {
              func.apply(context, args);
              previous = now;
            }
          } else if (type === 2) {
            if (!timeout) {
              timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args);
              }, wait);
            }
          }
        };
      }
      content.onmousemove = throttle(count, 1000, 2);
    </script>
  </body>
</html>

👨‍🌾 clearTimeout(timer)和timer=null的区别

clearTimeout(timer)清除了timer指向的定时器。根据测试,timer是分配一个随机数字id,clearTimeout后,只是定时器停止了,timer的变量指向数字id还在,也就是说,timer对象本身还是存在的,timer的值是一个id,比如这样就可以重启:

var timer=setTimeout(function(){abc()},3000);
//鼠标放上去关闭,离开再次启动
$("div").mouseover(function(){
clearTimeout(timer);//关闭
}).mouseout(function(){
timer=setTimeout(function(){abc()},3000) ;//重新启动
});

所以,clearTimeout(timer)达到保留对象的作用以便于再次使用这个id对应的定时器,但是如果是timer=null,虽然也同样达到停止的效果,但是本身的timer对象id已经被清空了,timer会被系统回收,无法再次启动原来的timer;再次使用需要重新定义一个新对象var timer=setTimeout(function(){abc()},3000);但这个ID值不等于上一个的timer值,这里是两个timer,不相等,个人感觉区别不大,但是原理是不同的。

timer = null 只是将定时器timer的指向改为null,即timer这个变量不指向某个定时器了,然而并没有在内存中清除这个定时器,定时器还是会如期运行,定时器依旧可以使用。把timer赋值为null,是为了释放内存,同时也方便布尔判断

如同在debounce函数中将timer = null并不能达到防抖的目的,因为每个定时器都只是将内存地址指向了null,而每个定时器都将会执行一遍。而clearTimeout(times)会将定时器从内存中清除掉。另外关于定时器是否需要用完清除的问题,具体还得看需求,如果是很少个数的定时器,可以不清除;如果数量很多或者数量不可控,则必须要做到手动清除,否则定时器将会非常占用电脑cpu,非常影响性能。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

儒雅的烤地瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值