JavaScript 防抖(debounce) 和 节流(throttling)

此篇文章用于记录笔者学习防抖节流的理解,十分感谢 @起晚的蜗牛防抖(debounce) 和 节流(throttling),这篇文章帮助了笔者如何写出一个更高效的防抖,建议本篇的读者可以先浏览 @起晚的蜗牛 的这篇博客,其在他的文章中作了一些便于读者理解的图,本博客也将使用他的博客中的一部分图文。
注意,本篇文章未标原创,仅仅是笔者的一篇学习笔记,其中的内容是在上文提及的博客的基础上进行的理解梳理。
原文中的防抖高性能写法笔者也进行了思路分析,便于读者理解。

1. 防抖节流有什么用?

假设我们为当前页面添加了下面的监听函数,当页面滚动时,就会触发showTop,这时候,我们如果仅仅是按一下方向键使页面向下滚动一点点,我们就能在控制台看到输出的一长串信息:

function showTop  () {
      var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
      console.log('滚动条位置:' + scrollTop);
}
window.onscroll = showTop

控制台:
在这里插入图片描述
而我们现在的showTop仅仅是向控制台输出一条打印信息,试想,如果我们需要在页面滚动的时候向后台发送一个请求,那么,这时候就出现了请求过于频繁,然而实际上我们并不需要如此高频的反馈!这会牺牲我们前台和后端以及网络的性能。
而防抖节流就可以在一定程度上避免上文中的情况。

2. 防抖(debounce)

策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。 这是debounce的基本思想,在后期又扩展了前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。
延迟debounce,示意图:
在这里插入图片描述
前缘debounce, 示意图:
在这里插入图片描述

2.1 延迟防抖的简易实现

我们可以使用一个定时器来执行showTop,在周期期间若showTop再次被触发,那么清楚之前的定时器并重新创建一个定时器:

function debounce(fn,delay){
 let timer = null
 return () => {
    if (!timer) {	// 如果定时器不存在,就创建一个定时器来执行fn
      timer = setTimeout(()=>{
        fn()		// 当执行完fn之后,需要将timer 设置为null
        timer = null
      },delay)
    }else {
      clearTimeout(timer)
      timer = setTimeout(()=>{
        fn()
        timer = null
      },delay)
    }
  }
}
function showTop  () {
  let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000)

这也是我一直实现防抖的方式,但是,在阅读了 @起晚的蜗牛 的博客之后,我才发现,通过这种方式来实现防抖,虽然也达到了降低触发频率的期望,但是,客户端需要频繁的创建和清除定时器,看似达到了我们的需求,实则是拆东墙补西墙,损耗了客户端的性能。

2.2 延迟防抖的高性能实现

为了客户端不需要频繁地创建清除定时器来实现防抖,我们可以使用时间戳。
周期内有新事件触发时,重置定时器开始时间撮,定时器执行时,判断开始时间撮,若开始时间撮被推后,重新设定延时定时器。

function debounce(fn,delay){
	 let timer = null
	 let triggerTime // 触发fn的时间
	
	 let run = (wait) => {
		   timer = setTimeout(()=>{
		     let executeTime = (new Date()).getTime() // 执行fn的时间
		     let alreadyWait = executeTime - triggerTime
		     if (alreadyWait < wait){
		       triggerTime = executeTime
		       run(delay - alreadyWait)
		     } else {
		       fn()
		       timer = null
		     }
		   }, wait)
	 }
	
	 return () => {
		   triggerTime = (new Date()).getTime()	//重置fn的触发时间
		   if (!timer) {
		     run(delay)
		   }
	 }
}

function showTop  () {
 let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
 console.log('滚动条位置:' + scrollTop);
}

window.onscroll = debounce(showTop,3000)

这里的debounce方法理解起来存在一定难度,读者可以结合下图进行理解。
在这里插入图片描述

  1. 当在时间1触发事件时,会执行:
   triggerTime = (new Date()).getTime()	//重置fn的触发时间
   if (!timer) {
     run(delay)
   }

因为此时的timer=null,所以执行run(delay)
2. 如果在时间1和时间2之间,我们没有再次触发事件,那么,我们将在时间2执行fn(注意,我们忽略setTimeout的误差,实际情况中时间2-时间1>=delay间隔,这里我们姑且认为时间2-时间1=delay间隔,这里的误差不影响我们的思路),也就是alreadyWait=wait,定时器执行else分支中的流程:

if (alreadyWait < wait){
	run(delay - alreadyWait)
 } else {
 	fn()
   	timer = null
 }
  1. 但是,如果我们在时间1到时间2之间再次触发了事件,这会更新triggerTime,在时间2时,我们将执行run(delay - alreadyWait),也就是再次构建一个定时器,等待时间为delay-alreadyWait;一般的防抖是在事件的触发时间,即时间3,来重新创建一个定时器,等待时间为delay。而现在,我们是在时间2的时候来创建定时器,所以等待时间由delay => delay - alreadyWait

为什么要这样处理呢?
时间1到时间2之间无论我们触发了多少次事件,因为这段时间内的!timer
的值为false,所以这段时间内触发的事件响应只是更新了triggerTime,而当我们在时间2执行定时器的内容时,只会获取到最新的triggerTime(也就是时间1到时间2期间最后一次事件触发时间),由于triggerTime的改变,定时器会执行run(delay - alreadyWait),这时候才会创建一个新的定时器。所以说,无论我们在时间1到时间2之间触发了多少次该事件,定时器也最多也只会创建两次,性能得到了优化。

2.3 前缘防抖的高性能实现

前缘防抖和延迟防抖的实现类似:
immediate表示是否使用前缘防抖

    function debounce(fn,delay,immediate=false){
      let timer = null
      let triggerTime // 触发fn的时间

      let run = (wait) => {
        console.log('create timer')
        timer = setTimeout(()=>{
          let executeTime = (new Date()).getTime() // 执行fn的时间
          let alreadyWait = executeTime - triggerTime
          if (alreadyWait < wait){
            run(delay - alreadyWait)
          } else {
            if (!immediate){
              fn()
            }
            timer = null
          }
        }, wait)
      }

      return () => {
        triggerTime = (new Date()).getTime()
        if (!timer) {
          if (immediate){
            fn()
          }
          run(delay)
        }
      }
    }

3. 节流(throttling)

throttling,节流的策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期。

延迟throttling示意图:
在这里插入图片描述
前缘throttling 示意图:
在这里插入图片描述
immediate表示是否采用前缘节流

    function throttling(fn,delay,immediate=false){
      let timer = null

      return () => {
        if (!timer){
          if (immediate){
            fn()
          }
          timer = setTimeout(()=>{
            if (!immediate) fn()
            timer = null
          },delay)
        }
      }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值