防抖(debounce)和节流(throttle)

最近我在面试一些5年的前端,问他们有哪些对闭包的应用,竟然一个都搭不上来,问他们用过防抖节流吗,知道函数柯里化吗,如何实现一个bind嘛,他们说用过,但不会写。
作为一个5年的前端,这样真的不行,他们我们从思路上去讲防抖和节流的做法

一、防抖(debounce)

首先我们介绍一下什么是防抖,防抖就是当多次事件产生时,只需要最后一次真实的触发事件

最经典的应该是,模糊匹配搜索,即用户在搜索框里输入,我们会去模糊搜索出用户可能想搜索的内容的列表

这个时候为了减少搜索频次,我们就会加入防抖,以便在用户停在输入的一段时间后再触发搜索,减少服务端压力

说完防抖的应用场景,我们来说一下思路吧。

其实思路很简单,我们希望在一段时间内的所有事件都只在最后一次事件触发的N秒后再执行最后一次事件,那这里我们就要利用setTimeout来做,并且要用到闭包的一个特性 —— 数据不会被释放的特性

下面先上代码

 <div id="app">
   <input id="input" />
 </div>
 <script type="text/javascript">
   function debounce(cb, wait = 300) {
     let timer = null;
     return function () {
       if (timer) {
         clearTimeout(timer);
         timer = null;
       }
       timer = setTimeout(() => {
         cb.apply(this, arguments);
       }, wait);
     };
   }
   document.getElementById("input").addEventListener("input",debounce(function(e){
     console.log(e)
   },1000));
 </script>

代码分析:

1.传入回调函数以及希望的时间间隔,把定时器存到变量里,返回一个函数,函数内部调用定时器的变量;
2.如果定时器存在了,则关闭上次的定时器,防止多次触发
3.设置定时器,并且存到变量里去,当定时器执行时,执行回调函数,并且把this指向调用返回函数的对象(这里如果不了解 this指向 以及 arguments 的小伙伴,赶紧看看我之前关于 this指向 的文章吧)
4.监听元素上的事件,并设置响应函数为执行防抖函数返回的函数,当事件发生时,元素就会调用返回的函数了,此时函数内部的this指向了元素对象
ps:上面的 arguments 不懂的赶紧百度

二、节流

什么是节流呢?

节流就是希望在一段时间内的有且只触发一次事件

下面我们介绍两种场景:

  • 滑动页面的时候,希望有个回顶部的按钮会根据滑动的位置控制显隐,以及有过渡效果,例如滑动此文章到底部,能看到有个回顶部的按钮或显示或隐藏 文章会顶部按钮

  • 水果忍者这款游戏里,手指移动的时候去切割水果,可以控制在一个时间内去处理手指坐标,控制一个流畅度,减少dom元素的操作

介绍完场景,接下来上代码,实现节流的方式有两种:

一种依然是使用定时器做,存在定时器则不做操作,不存在定时器则设置一个定时器并利用闭关存起来,当定时器触发后,触发回调函数并释放掉定时器的变量

function throttle2(cb, wait = 300) {
   //定时器版本
   let timer = null;
   return function () {
     if (!timer) {
       timer = setTimeout(() => {
         cb.apply(this, arguments);
         timer = null;
       }, wait);
     }
   };
 }

一种是比较时间戳,利用闭关存最后一次触发的时间戳,每次触发对比一下距离最后一次触发的时间是否大于时间间隔,是则触发回调函数,并且更新最后一次触发时间为此时的时间

function throttle1(cb, wait = 300) {
   //时间戳版本
   let lastTime = null;
   return function () {
     const nowTime = new Date().getTime();
     if (!lastTime || nowTime - lastTime > wait) {
       lastTime = nowTime;
       cb.apply(this, arguments);
     }
   };
 }

但是以上的两种方式有一定的缺陷:

定时器版本的,由于是一段时间后才触发回调函数,第一次事件发生时不会先执行一次事件,像上面水果忍者那个案例这显然是不合适的,只适用于回顶部按钮的那个案例(小程序端这个场景还是有缺陷,后面有个优化版本)

时间戳版本的,由于大于时间间隔才能触发,所以很有可能最后一次触发的事件是不会发生的,像上面的回顶部按钮是不适合的,会导致回顶部按钮的按钮位置出现不一致

因为上面的两个版本其实都不合适水果忍者的案例,所以我们可以结合两者做一个混合版本的节流

function throttle(cb, wait = 300) {
  //混合版本
   let lastTime = null;
   let timer = null;
   return function () {
     const arg = arguments;
     const nowTime = new Date().getTime();
     if (!lastTime || nowTime - lastTime > wait) {
       if (timer) {
         clearTimeout(timer);
         timer = null;
       }
       lastTime = nowTime;
       cb.apply(this, arguments);
     } else if (!timer) {
       timer = setTimeout(() => {
         cb.apply(this, arguments);
         timer = null;
       }, wait);
     }
   };
 }

三种方案,你们可以根据场景选择,下面上一段代码验证一下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>throttle</title>
    <style>
      #app {
        margin: auto;
        width: 50vw;
        height: 500vh;
        background-color: antiquewhite;
        text-align: center;
      }
      #toTop {
        position: absolute;
        width: 80px;
        height: 80px;
        border-radius: 80px;
        background-color: cadetblue;
        bottom: 20vh;
        right: 30vw;
        transition: transform 1s;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div id="toTop"></div>
    </div>
    <script type="text/javascript">
      function throttle(cb, wait = 300) {
        //混合版本
        let lastTime = null;
        let timer = null;
        return function () {
          const nowTime = new Date().getTime();
          if (!lastTime || nowTime - lastTime > wait) {
            if (timer) {
              clearTimeout(timer);
              timer = null;
            }
            lastTime = nowTime;
            cb.apply(this, arguments);
          } else if (!timer) {
            timer = setTimeout(() => {
              cb.apply(this, arguments);
              timer = null;
            }, wait);
          }
        };
      }
      function throttle1(cb, wait = 300) {
        //时间戳版本
        let lastTime = null;
        return function () {
          const nowTime = new Date().getTime();
          if (!lastTime || nowTime - lastTime > wait) {
            lastTime = nowTime;
            cb.apply(this, arguments);
          }
        };
      }
      function throttle2(cb, wait = 300) {
        //定时器版本
        let timer = null;
        return function () {
          if (!timer) {
            timer = setTimeout(() => {
              cb.apply(this, arguments);
              timer = null;
            }, wait);
          }
        };
      }
      (function () {
        document.addEventListener(
          "scroll",
          throttle2(function (e) {
            console.log("-------------");
            console.log(e.timeStamp);
            console.log(document.documentElement.scrollTop);
            document.getElementById(
              "toTop"
            ).style.transform = `translateY(${document.documentElement.scrollTop}px)`;
          }, 1000)
        );
      })();
    </script>
  </body>
</html>

最近小程序的实现滑动节流,因为小程序的滑动位置是通过监听滑动函数onPageScroll返回的参数获取的,如果还用上面的定时器版本throttle2显然会造成获取到的参数可能是时间段开始的旧数据,因此我们得加个参数记录时间段最后一次触发函数的参数

function throttle3(cb, wait = 300) {
  //定时器优化版本
  let timer = null;
  let lastArg = null
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        cb.apply(this, lastArg || arguments);
        lastArg = null
        timer = null;
      }, wait);
    } else {
      lastArg = arguments
    }
  };
}
//小程序代码
onPageScroll: throttle3(function(e) {
  let scrollTop = e.scrollTop
  console.log('scrollTop', scrollTop)
  this.scrollTop = scrollTop 
},2000)
//设置时间长一点可以对比一下优化前后的定时器版本

总结:防抖和节流就是最经典的闭包案例,相关的闭包案例还有很多,感兴趣的可以自行百度了解

ps:看完上面的分析和代码,如果还不清晰可以留言,但是请小伙伴一定要掌握,这其实是每个前段同学成长路上一定要懂的知识点,如果你不想原地踏步,不想工作5年后还只是一个初级前段,那要把握每一次知识的掌握,加油!!!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值