手把手教会你防抖与节流

202 阅读7分钟

🏝️前言

防抖和节流作为面试的常问点之一,也作为项目开发中常需要考虑的问题,你还不知道防抖节流是什么吗?

不用担心,今天这篇文章教会你JS中的防抖与节流

曾看到一个对于防抖节流非常贴切的形容,相信大部分人都玩过王者或者联盟吧,这个形容就和它们有关

大家可以把防抖理解为回城,把节流理解为技能冷却

不太玩游戏的朋友也不用担心理解不了,下面我们就来详细解说

🏝️防抖

🏷️防抖是什么

我们先从防抖开始讲解,防抖是什么呢?为什么说可以把它理解为游戏里的回城呢?

在游戏里回城时间需要8秒,当你按下回城按钮,你需要等待8秒后才能成功回城,但如果你中途不停重新触发回城,这个8秒的倒计时就会不断的中断重计时、中断重计时...

直到某一次,你点下了回城,然后站在原地不动,在这8秒的倒计时内,回城没有被一次次触发,那么这个倒计时就不会中断,8秒结束后就可以成功回到泉水了

但是这个例子其实不太准确,另外再补充一个吧,就说手机上的手写键盘,如果我们每写一笔就识别一次,根本得不到我们想要的文字

这时就可以用到防抖,当我们写完某一笔后0.5秒内不再写了,那就会识别我们已写好的文字,但若写完某一笔后0.5秒内又写了一笔,那就重新计时

其实防抖的原理就和这个差不多,在n秒后执行该事件,若在n秒内被重复触发,则重新计时

🌰例子来啦

那现在我们先来手写一个防抖函数,然后再根据代码逐行解释

<body>
  <div class="out">
    <div class="box">0</div>
    <button>点我加1</button>
  </div>
</body>
<script>
  const btn = document.querySelector('button')
  const box = document.querySelector('.box')
  let i = 1
  const addFn = function() {
    console.log(this)
    this.innerHTML = i++
  }

  // 防抖
  const debounce = (fn, ms = 0) => {
    let timer
    return function(...args) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn.call(this)
      }, ms);
    }
  }

  btn.addEventListener('click', debounce(addFn, 1000).bind(box))
</script>

代码写好了,我们来看看实现效果

防抖.gif

可以看出,当我们不停点击按钮时,数字并没有加1,然而当某一次点击停下超过1秒了,期间没有再次点击,数字才会成功加1

这也正体现了防抖的在n秒后执行该事件,若在n秒内被重复触发,则重新计时这一原理

🏷️详解

我们再来对script中每一行代码都是什么意思进行解释:

  // 获取按钮和显示框
  const btn = document.querySelector('button')
  const box = document.querySelector('.box')
  // 定义变量i,存放数字
  let i = 1
  // 让数字加1的函数
  const addFn = function() {
    // 我们要让this指向box盒子,让box里的数字变化
    // 在控制台打印this,方便我们观察this指向的元素是否正确
    console.log(this)
    // 执行一次数字加1
    this.innerHTML = i++
  }

这几行代码较为简单,下面开始防抖部分

  // 定义防抖函数debounce
  // 传入的参数fn是实现数字加1的函数,ms是防抖的倒计时n秒,可以给它一个默认值0
  const debounce = (fn, ms = 0) => {
    // 声明一个定时器标记timer
    let timer
    // 直接将function作为返回值返回
    return function() {
      // 每点击一次按钮,就会触发一次防抖函数,那么当防抖函数被触发了,我们第一件事应该是关闭定时器停止倒计时
      // 可以联想一下玩游戏回城时,回城过程中重新触发了回城,那上一次的回城倒计时应该先停止,再重新开始倒计时
      clearTimeout(timer)
      // 这里就是重新开启定时器,再次1000ms倒计时
      // 只要我们不停点击按钮,就会不停触发防抖函数,1000ms倒计时还没结束就又会清除计时器重新赋值
      // 直到某一次1000ms内都没有再次点击,才能成功执行计时器,数字加1
      timer = setTimeout(() => {
        // 使用call()改变this指向,不改this指向的话,这里的this就不指向box了,addFn函数的this.innerHTML就不会执行
        fn.call(this)
      }, ms);
    }
  }
  
  // 给按钮btn绑定点击事件,每点击一次就去执行调用防抖debounce函数
  // 传入实参为addFn数字加1的函数,以及1000ms即1s的计时时间
  // bind也是用来改变this指向的,让this指向的是显示数字的box盒子
  btn.addEventListener('click', debounce(addFn, 1000).bind(box))

🏷️this指向补充

因为代码里用到了两个改变this指向的函数call()和bind(),那这里就顺便补充一下它们的用法

call()

语法:function.call(this指向的目标, 指定的参数列表)

call()方法接受的是一个参数列表

bind()

语法: function.bind(this指向的目标, 指定的参数列表)

bind()方法创建一个新的函数,这个函数里面的this指向我们传入的第一个参数;bind不是立即执行的,需要手动调用

到这里防抖就差不多啦😊,接下来我们再来谈谈节流吧

🏝️节流

🏷️节流是什么

我们在开头说防抖是回城,节流是技能冷却,那这又是个什么意思呢?

好的,现在思绪进入游戏中,假如一开局手残,不小心点了个闪现😈,那在接下来120s的冷却时间内都不能再次触发闪现,在冷却时间内不管你怎么点,闪现都只触发那一次,除非等到120s结束,才能再次使用

延申到节流的概念,节流是n秒内只执行一次,若在n秒内重复触发,只有一次生效

其实实现节流有两种方式,一种是和防抖一样使用定时器,另一种就是时间戳;上边刚用过定时器,这里就用时间戳来实现吧

🌰例子来啦

那下面上代码:

<body>
  <div class="out">
    <div class="box">0</div>
    <button>点我加1</button>
  </div>
</body>
<script>
  const btn = document.querySelector('button')
  const box = document.querySelector('.box')
  let i = 1
  const addFn = function() {
    console.log(this)
    this.innerHTML = i++
  }

  // 节流
  const throttle = (fn, ms = 0) => {
    let pre = 0
    return function(...args) {
      let now = Date.now()
      if (now - pre >= ms) {
        fn.apply(this)
        pre = Date.now()
      }
    }
  }

  btn.addEventListener('click', throttle(addFn, 1000).bind(box))
</script>

好的,再来看看实现的效果:

节流.gif

从实现效果上可以看出,节流与防抖不同的是,当我们不停点击按钮时,它是有节奏地加1的,是每隔一个时间段执行一次加1,而不是等点击结束再去计算时间

🏷️详解

这个代码除了节流throttle函数部分与上一个代码不同,其余相同,就不进行解释了,这里主要说说节流部分的代码

  // 定义节流函数throttle
  // 传入的参数fn是实现数字加1的函数,ms是时间段n秒,n秒内只执行一次数字加1
  const throttle = (fn, ms = 0) => {
    // 定义时间戳起点
    let pre = 0
    return function() {
      // 鼠标一点击按钮,就记录下当前的时间戳
      let now = Date.now()
      // 当时间戳now与时间戳起点的时间间隔大于ms时,执行回调函数
      if (now - pre >= ms) {
        // 绑定this,确保this的指向正确
        fn.apply(this)
        // 将当前时间记录下来,作为下一次计时的起点
        pre = Date.now()
      }
    }
  }
  
  // 给按钮btn绑定点击事件,每搁1000ms调用一次addFn函数,让数字加1
  // 同时使用bind()函数让this指向box
  btn.addEventListener('click', throttle(addFn, 1000).bind(box))

🏷️this指向再补充

在上边的防抖代码里我们使用了call()和bind()这两个改变this指向的方法,在节流的代码里我们又使用另一种改变this指向的方法apply()

就再对apply()进行一个补充说明吧

apply()

语法:function.call(this指向的目标, 数组参数)

虽然这个函数的语法与 call()几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组

🏝️结语

这次对JS中防抖和节流的总结与补充差不多就到这里啦,要是大家能在这篇文章里有所收获的话那可再好不过啦😊

要是有什么错误的话也欢迎指出哦,大家一起进步🍻