实现防抖、节流函数

636 阅读6分钟

认识防抖函数和节流函数

防抖函数

  • 当事件触发时,相应的函数不会立即触发,而是等待一段时间。
  • 当事件连续触发时,函数的触发等待时间会被不断重置(推迟)。 通俗的讲,防抖就是,每次触发事件时,在一段时间后才真正响应这个事件。

图片.png

防抖函数的应用场景很多,很多情况我们并不希望这些事件重复触发:

  • 输入框中频繁输入内容,如果输入框改变一次就发送一次请求的话,会对服务器造成很大的压力,所以我们希望在连续输入的时候不发送请求,直到用户输入完或者一段时间没有继续输入的话才发送请求。
  • 频繁点击按钮触发事件(恶意的行为)。
  • 用户缩放浏览器时频繁触发resize事件。

节流函数

  • 如果事件被频繁出发,那么节流函数会按照一定的频率来执行函数。
  • 不管中间触发了多少次,执行函数的频率总是固定的

图片.png

实现防抖函数

在实现防抖函数之前,我们介绍一下闭包的概念,很多人可能会问,防抖函数跟闭包有啥关系,闭包又是啥?不急,我们下面就介绍。

闭包

闭包的产生是跟js执行有关的。

  • js代码在执行之前,会扫描全局代码,同时创建一个GlobalObject对象,里面挂载了一些内置的类和函数以及window。

  • 扫描全局代码的过程中,如果发现var声明的变量,会在GO对象中挂载一份这个变量,且值为undefined(这就是声明提前)。

  • 如果扫描到函数声明的话,由于这个函数后面可能用不到,所以v8引擎只对函数作预解析,预解析指的是不会对函数内部进行解析,只会在堆内存中创建函数对象,这个函数对象中保存了两个东西:父级作用域parentScope以及函数内部的代码块

  • 预解析完之后,会在GO对象中挂载该函数对象,也就是保存一份该函数对象的地址引用

  • 扫描完代码之后,进入执行阶段。js代码执行是在执行上下文栈中执行的,所以我们要生成GO对象的执行上下文。生成执行上下文分为两个步骤:创建Variable Object对象指向GO,执行js代码。

  • 在执行代码的时候,就会依次对变量的声明进行赋值。

  • 如果执行代码的时候,执行了函数,就会对函数内部进行进一步解析。具体过程跟前面很相似:创建Activation Object,并且扫描代码,如果扫描到函数的话,也会在堆内存中创建函数对象(包括两部分:parentScope以及代码块),并且将这个函数对象的地址挂载到AO上。

  • 解析完函数内部的代码之后,要执行函数内部的代码了,这时候会创建函数执行上下文入栈,创建的过程也包括两部分:创建VO指向AO,执行代码。在执行完函数的时候,执行上下文出栈,AO随之销毁。

  • 但是有一种情况,AO对象是不会被销毁的,那就是如果函数返回了函数内部声明的函数,并且在全局中可以根据某个变量逐渐找到这个返回的函数,那么AO就不会销毁。因为函数对象中是保存了parentScope的,如果能找到这个函数对象的话,那么就一定能找到parentScope指向的AO对象,也就能获取到AO对象中挂载的变量。

简单的说,如果函数能访问外层作用域的变量,那么这就是一个闭包。闭包往往在保存私有变量方面特别管用,但是如果使用不当也会造成内存泄漏等情况。。。

初步实现防抖函数

防抖函数的实现是,传入一个函数以及一个时间,返回一个防抖化的函数。利用闭包保存定时器,进而实现这个防抖化的效果。

function debounce(fn,time){

    //利用闭包,定义定时器,返回的函数内部可以获取到这个变量
    let timer 
    return function(...args){  //返回一个可以接收参数的函数    

        //每次触发函数之前,重置定时器
        clearTimeout(timer)
        
        timer = setTimeout(()=>{
            fn.apply(this,args) //执行函数
        },time)
    }
}

第一次立即执行

如果我们希望,第一次触发防抖函数的时候,函数立即响应,后续触发函数的时候才进行防抖,可以用下面这种方法实现。

function debounce(fn, time, immediate = true) {
  let timer;
  //标记是否立即调用过
  let isInvoke = false
  return function (...args) {
    //如果没有立即调用过,并且需要立即调用的话
    if(!isInvoke && immediate){
        fn.apply(this, args); 
        isInvoke = true
    }else{
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, args); 
          isInvoke = false  //不要忘了在防抖函数结束的时候重置isInvoke哦~
        }, time);
    }
  };
}

实现节流函数

初步实现节流函数

实现节流函数的话,只需要记录上次触发函数的时间戳和当前的时间戳,进行比较即可。

function throttle (fn,time){
    //保存上次执行的时间戳
    let lastTime = 0
    return function(...args){
        //获取当前的时间戳
        let nowTime = new Date().getTime()
        //如果两次触发的时间间隔大于了time,就执行
        if(nowTime - lastTime >= time){
            fn.apply(this,...args)
            lastTime = nowTime
        }
    }
}

第一次是否立即执行

可以看到上面我们实现的节流函数中,因为lastTime是0,所以第一次执行返回的函数的时候,是会立即执行的。如果我们希望能控制第一次是否立即执行的话,可以这么实现。

//leading为false表示不想开头立即触发
function throttle(fn, time, options = { leading: false }) {
    const { leading } = options
    let lastTime = 0
    let timer
    return function () {
        const nowTime = new Date().getTime()
        //如果不希望第一个触发的是立即执行的话,就设置lastTime为nowTime
        //这样的话时间间隔就会从0开始计算,直到时间间隔大于给定的time
        
        //lastTime === 0 其实可以用于表示是不是一段连续触发事件的第一个事件
        if(lastTime === 0 && leading === false) lastTime = nowTime 
        const remainTime = time - (nowTime - lastTime)
        if (remainTime <= 0) {
            fn.apply(this)
            lastTime = nowTime
        } 
    }
}

最后一次是否执行

这是关于,如果最后一次触发的时间点位于两个触发频率结点的中间时,要不要触发的问题。上面的实现中,如果最后一次触发的时间点位于两次周期的中间的话,是不会触发的。如图。

图片.png

那么我们如果希望最后一次在中间的时候要触发,该怎么做呢?我们可以在每次周期开始的时候设定一个定时器,如果最后一次是在两个周期中间触发的话,定时器会在这个时间的末尾执行的。

function throttle(fn, time, options = { leading: false, trailing: true }) {
  const { leading, trailing } = options;
  let lastTime = 0;
  let timer;   //保存定时器
  return function (...args) {
    const nowTime = new Date().getTime();
    if (lastTime === 0 && leading === false) lastTime = nowTime;
    const remainTime = time - (nowTime - lastTime);
    if (remainTime <= 0) {
      //如果执行函数的时候,时间间隔大于time,就执行一次
      //如果有定时器的话,我们清除掉定时器,因为我们不希望定时器执行函数
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      lastTime = nowTime;
    } else if (trailing && !timer) {
      //我们只需要设置一个定时器就足够了
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null//在定时器触发函数的时候,不要忘了重置timer和lastTime
        //如果不希望下一次执行时第一次触发的话,需要将lastTime设置为0
        //设置为0的时候,会在前面经过判断,重新设置为nowTime的
        lastTime = !leading ? 0 : new Date().getTime() + time;
      }, remainTime);
    }
  };
}