节流,防抖函数 | 青训营笔记

115 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第3天

引入

防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

我们知道JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。

但是对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;

防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。

但是很多前端开发者面对这两个功能,有点摸不着头脑:

1.某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到)?

2.某些开发者可以区分,但是不知道如何应用?

3.某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写?

接下来我们会一起来学习防抖和节流函数:

我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;

1.什么是防抖、节流函数

1.1 防抖函数

定义:给一个固定时间,如果你开始触发动作,并且在这个固定时间内不再有任何动作,我就执行一次,否则我每次都会重新开始计时。我们可以用极端情况理解它:如果给定时间间隔足够大,并且期间一直有动作触发,那么回调就永远不会执行。

举例:比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。

如果在五分钟的时间内,没有同学问我问题,那么我就下课了;

在此期间,同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;

如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);

作用范围:

  • 可用于input.change实时输入校验,比如输入实时查询,你不可能摁一个字就去后端查一次,肯定是输一串,统一去查询一次数据。
  • 可用于 window.resize 事件,比如窗口缩放完成后,才会重新计算部分 DOM 尺寸

1.2 节流函数

定义:用户会反复触发一些操作,比如鼠标移动事件,此时只需要指定一个“巡视”的间隔时间,不管用户期间触发多少次,只会在间隔点上执行给定的回调函数。 我们同样可以用极端情况来理解:如果给定的间隔时间是 240毫秒,用户永不间断地在屏幕上疯狂移动鼠标,那么你的回调函数会分别在 240毫秒、 480毫秒、 720毫秒... 就这么一直执行下去

举例:比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会 解答一个问题; 如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;

作用范围:用于监听 mousemove、 鼠标滚动等事件,通常可用于:拖拽动画下拉加载

2手写防抖函数

2.1基本防抖函数

基本逻辑:以input输入框为例,监听inputChange事件,内置一个定时器,每一次输入都会延迟自定义的时间才会生效,当我们输入时,如果在此事件的定时器的延迟时间内,再次触发该事件,则取消掉之前的定时器效果,以此类推,每一次的事件都会取消掉上一次的定时器,因此,只有最后一次的定时器才会生效

// 版本1 常规防抖函数 用于实时在线编辑文件保存

function debounce1(fn, delay) {
let timer = null;
//真正执行的函数
const _debounce = function (...args) {
 if (timer) {
   clearTimeout(timer);
 }
 timer = setTimeout(() => {
   fn.apply(this, args);
 }, delay);
};
return _debounce;
}

每次执行该函数都会先清除上一次的定时器(timer)

注意:当用户传递参数时,要将参数保存下来并传入真正要执行的函数

此时fn执行时this指针会改变,因此真正调用时要用fn.apply将this改回原本的this指向(一般都指向该事件)

2.2 函数进阶 用户自定义是否立即执行

设置三个参数,具体执行的函数(如监听,网络请求等),延迟时间,是否立即执行(布尔值)

debounce.cancel,指在设定的延迟时间内不想再执行函数,则直接调用此方法即可。

// 版本2 第一次输入立即执行,后面的正常防抖动(如百度搜索)
// immediate:用户传参决定是否立即执行

// 只有isInvok为fasle时会触发立即执行

function debounce2(fn, delay, immediate) {
   let timer = null;
   //用于判断是否执行过
   let isInvoke=false
 //真正执行的函数
 const _debounce = function (...args) {
   if (timer) clearTimeout(timer);
   if (immediate && !isInvoke) {
       fn.apply(this, args);
       isInvoke=true
   } else {
     timer = setTimeout(() => {
         fn.apply(this, args);
         isInvoke=false
     }, delay);
     }
     //封装一个取消函数 (函数对象)
     _debounce.cancel= function() {
         if (timer) clearTimeout(timer)
         timer = null;
         isInvoke=false
     }
   };
   

 return _debounce;
}

3手写节流函数

3.1 基本节流函数

只实现最基本的节流功能

image.png 基本逻辑:先设置一个lasttime,每次发送请求的获取到最新的时间戳,用最新的时间戳减去lasttime则为每次事件的时间差

当连续触发事件时 节流函数的执行与interval类似

只有当时间戳大于或等于设置的延迟时间,则会执行函数,并将最新的lasttime设置为此次触发的时间

function throttle1(fn, interval) {
let lastTime = 0;
const _throttle = function () {
 const nowTime = new Date().getTime();
 //判断每一次调用函数的时间减去上一次触发函数的时间间隔(第一次默认为0)
 // 与传入的时间参数对比,大于传入的时间参数则触发函数
 const remainTime = interval - (nowTime - lastTime);
 if (remainTime <= 0) {
   fn();
   lastTime = nowTime;
 }
};
return _throttle;
}

3.2 节流进阶

版本2 实现leading

即用户自行设定是否当第一次输入时就立即执行

通过leading的布尔值来决定, 如果为true,则第一次触发事件时,会使当前时间差与设定的延迟时间相同:那么会执行函数


function throttle2(fn, interval, options = { leading: true, trailing: false }) {
  const { leading, trailing } = options;
  let lastTime = 0;
  const _throttle = function () {
    const nowTime = new Date().getTime();
    //判断 第一次不会执行(会等待延迟时间)
    if (!lastTime && !leading) lastTime = nowTime;
    //此时remainTime>0,第一次则不会默认执行
    const remainTime = interval - (nowTime - lastTime);
    if (remainTime <= 0) {
      fn();
      lastTime = nowTime;
    }
  };
  return _throttle;
}

版本3 实现trailing(最后一次调用函数会等待延迟时间并执行) 默认是不会执行的

每一次执行函数时都生成一个定时器,延迟为触发事件时的时间差,并在该定时器中执行函数,此时每一次都会执行两次函数,因此每一次都需要判断是否有定时器,如果有则清除掉,但是当最后一次触发事件时,节流函数不再生效,此时就不需要再需要清除定时器,定时器中的函数会直接执行。

此时training生效



function throttle3(fn, interval, options = { leading: true, trailing: false }) {
    const { leading, trailing } = options;
    let timer = null;
  let lastTime = 0;
  const _throttle = function () {
    const nowTime = new Date().getTime();
    //判断 第一次不会执行(会等待延迟时间)
      
    if (!lastTime && !leading) lastTime = nowTime;
    //此时remainTime>0,第一次则不会默认执行
      
    const remainTime = interval - (nowTime - lastTime);
      if (remainTime <= 0) {
          //此判断用于当trailing的延迟调用还没开始(即在等待中)时,再次调用请求,则取消掉trailing
          if (timer) {
              clearTimeout(timer);
              timer = null;
        }
      fn();
      lastTime = nowTime;
      }
      //必须要没有定时器时才会生成定时器
    if (trailing&&!timer) {
        timer = setTimeout(() => {
            timer = null;
            lastTime=0
        fn();
      }, remainTime);
    }
  };
  return _throttle;
}