防抖+节流

105 阅读4分钟

一.防抖

1.解释

用于限制函数的执行频率,一定时间内多次触发同一个事件,只执行最后一次操作

2.应用

  • 搜索框中输入内容时,不会立即发送请求获取相关数据,而是等到文字输完后的一定时间向后端发请求显示出结果

3.手写防抖

(1)这个防抖函数要接收两个参数

  • 传入的执行函数fun
  • 延迟的时间delay(ms)

(2)防抖函数会返回一个函数,利用setTimeout延迟一段时间后调用传入的fun

(3)在每次定时器开始之前清空之前的定时器

function debounce(fun,delay) {
    let timeoutID 
    //返回一个函数
    return function(){
        //清除之前的定时器
        clearTimeout(timeoutID)
        //当 delay 时间到时,执行 fun
        timeoutID = setTimeout(fun,delay)//传入fun和delay,setTimeout会返回一个定时器id用于清楚定时器
    }
}

(4)this的指向与之前一致+正确接收参数(event)

 function debounce(fun, delay) {
        let timeoutID;
        //return返回的这个函数才是真正被调用的函数
        return function () {
          if (timeoutID) {
            clearTimeout(timeoutID);
          }
          timeoutID = setTimeout(() => {
            //改变this指向并把function的参数传给fun
            fun.apply(this, arguments);//fun的this的指向要与没加防抖时一致
          }, delay);
        };
      }

(5)增加立即执行机制

  • 根据timeoutID是否为空来判断是不是第一次执行
  • 要满足第一次立即执行过后,要在delay的时间过后再执行
 function debounce(fun, delay, immediate) {
        let timeoutID;
        //return返回的这个函数才是真正被调用的函数
        return function () {
          if (timeoutID) clearTimeout(timeoutID);

          if (immediate) {
            let action = !timeoutID;
            //设置immediate为true的情况下,在第一次过后到delay的这段时间内timeoutID都是有值的,就可以使得action为false,不会一直触发立即执行 ==>  即下一次触发函数最少要等到delay的时间后
            timeoutID = setTimeout(() => {
              timeoutID = null;
            }, delay);
            // 立即执行
            if (action) fun.apply(this, arguments);
          } else {
            //不立即执行
            timeoutID = setTimeout(() => {
              //改变this指向并把function的参数传给fun
              fun.apply(this, arguments); //fun的this的指向要与没加防抖时一致
            }, delay);
          }
        };
      }

二.节流

单位时间内触发多次事件,只执行一次

2.应用

  • 鼠标经过事件,页面缩放,scroll滚动等

3.手写节流

防抖函数的侧重点在于时间的间隔,延时delay后才能执行,而节流的重点在于对操作进行限制,一定时间只能执行一次

(1)时间戳写节流

利用当前时间-先前时间>delay

function throttle(fun, delay) {
        let prev = 0; //之前的时间戳

        return function () {
          //获取当前时间戳
          let cur = +new Date();
          if (cur - prev >= delay) {
            //更新时间戳
            prev = cur;
            //立即执行
            fun.apply(this, arguments);
          }
        };
      }

如果最后一次执行刚好在delay的时间内则不会执行(顾头不顾尾)

(2)定时器写节流

利用setTimeout延迟触发

function throttle(fun,delay){
        let timeoutID

        return function(){
            if(!timeoutID){
                timeoutID = setTimeout(()=>{
                    fun.apply(this,arguments)
                    //在执行时也要清除定时器id,不然一直存在无法再次调用
                    timeoutID = null
                },delay)
            }
        }
    }

这种虽然可以触发最后一次了,但是第一次触发事件可能等的很长

理想的情况是第一次触发而最后一次也触发

(3)定时器+时间戳写节流

//使用时间戳时顺便清除定时器,使用定时器时顺便更新时间戳
      function throttle(fun, delay) {
        let timeoutID;
        let prev = 0;
        return function () {
          let cur = +new Date();
          //时间戳部分
          if (cur - prev >= delay) {
            prev = cur;
            fun.apply(this, arguments);
            //消除定时器
            if (timeoutID) {
              timeoutID = null;
              clearTimeout(timeoutID);
            }
          }
          //定时器部分
          else if (!timeoutID) {
            timeoutId = setTimeout(() => {
              //记录最新的时间
              prev = +new Date();
              fun.apply(this, arguments);
              timeoutID = null;
            }, delay);
          }
        };
      }

其实还有一个问题,如果我们的delay=500ms而现在cur-prev=200ms明显未到执行的时间,所以会走到定时器部分,而定时器部分又要再延迟delay也就是500ms执行,真正的执行时间就变成700ms了,实际应该再等300ms就执行

解决方案 : 我们可以定义一个剩余执行时间remain,如果剩余执行时间<=0,那么就立即执行,如果不是,那么这个剩余时间应该成为接下来定时器的等待时间。

 //使用时间戳时顺便清除定时器,使用定时器时顺便更新时间戳
      function throttle(fun, delay) {
        let timeoutID;
        let prev = 0;
        return function () {
            //时间戳部分
          let cur = +new Date();
          //统计剩余时间
          let remain = delay - (cur - prev);
          //剩余时间<=0立即执行
          if (remain <= 0) {
            prev = cur;
            fun.apply(this, arguments);
            //消除定时器
            if (timeoutID) {
              timeoutID = null;
              clearTimeout(timeoutID);
            }
          }
          //定时器部分
          else if (!timeoutID) {
            timeoutID = setTimeout(() => {
              //记录最新的时间
              prev = +new Date();
              fun.apply(this, arguments);
              timeoutID = null;
            }, remain);
          }
        };
      }