JavaScript 防抖与节流

355 阅读3分钟

防抖与节流,它们的作用都是为了降低事件回调函数的调用频率,从而达到优化的目的。

需要防抖与节流??

我们一定需要防抖和节流吗??那么不使用防抖和节流会发生什么事情呢?

可以举几个栗子:

例子一:在Input输入框里,需要对用户的输入进行非法词汇判断?

对该Input进行onchange事件的监听,在用户每次有输入字符的时候,都会去调用相关接口进行判断。所以就会出现下面的情况

例子二:监听全局滚动事件,从而来实现比如,展示返回顶部、表格头部置顶,和其他改变某些元素的位置等等

监听window.onscrol,它的事件触发频率在大多数情况下都是异常的高,在它的事件处理函数中会存在触发大量的回流重绘,像获取document.body.scrollTop的值,如果不增加相关限制,会造成页面卡顿,带来较差的用户体验

判断是否需要防抖、节流的判断条件

  • 事件是否在持续触发
  • 事件在某个时间段是否有可能持续触发

通过这两个例子,可以看出我们要解决的问题—就是降低事件回调函数的执行频率。而所谓的防抖与节流,可以理解为,是在反复的尝试中,摸索出的一种有代表性的方案。

代表方案——防抖

这种方案可以实现的效果是:在某个时间段内如果连续触发事件,但是只有最后一次事件触发了回调函数。(注意这里不是停止触发就立即执行,而是有个delay的延迟) 该方案的关键在于判断,定时器是否存在,存在就清除

   function debounce(fn, delay) {
        let timer = null;
        return function () {
            let context = this;
            let args = [...arguments];
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
                fn.apply(context, args)
            }, delay);
        }
   }

在上面的第二个例子中,就可以这样使用

let fn = debounce(controlFn, 1000); // controlFn 具体的事件处理函数
window.addEventListener('scroll', fn);

// 或者
window.onscroll = debounce(controlFn, 1000);

代表方案——节流

效果:每隔一段时间就调用一次事件处理函数(在连续触发事件的过程中,只要到达规定的时间,就执行回调函数)

使用时间戳

function throttle(fn, delay) {
    let startTime = Date.now();
    return function () {
        let context = this;
        let args = [...arguments];
        let now = Date.now();
        if (now - startTime >= delay) {
            fn.apply(context, args);
            startTime = now;
        }
    }
}

使用定时器

throttle(fn, delay) {
    let timer = null;
    return function () {
        let context = this;
        let args = [...arguments];
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, args);
                timer = null;
            }, delay);
        }
    }
}

源码

understore.js有这么一段源码,我们一起来学习下~ 与我们前面写到的相比较,增加了immediate的参数,及相关判断是否要立即执行的逻辑

 // immediate 是否立即执行
  _.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArguments(function(args) {
      if (timeout) clearTimeout(timeout); // 防抖的处理
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });
    // 增加了取消定时器的操作
    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

关于restArguments 函数,就相当于兼容完成ES6中的函数参数的结构赋值

  var restArguments = function(func, startIndex) {
    // func.length 代表 func函数中定义的形参的数量
    startIndex = startIndex == null ? func.length - 1 : +startIndex;
    return function() {
      // arguments.length - startIndex 就是 从startIndex 参数到 arguments.length 的参数数量,在es6中 (a, b, ...rest)
      var length = Math.max(arguments.length - startIndex, 0),
          rest = Array(length),
          index = 0;
      // 对rest数组进行赋值,值是从arguments的startIndex开始取的
      for (; index < length; index++) {
        rest[index] = arguments[index + startIndex];
      }
      // 对 function (...l), function(a, ...l), function  (a, b,...l) 这三种进行特殊处理
      switch (startIndex) {
        case 0: return func.call(this, rest);
        case 1: return func.call(this, arguments[0], rest);
        case 2: return func.call(this, arguments[0], arguments[1], rest);
      }
      // 将arguments从0到startIndex的内容,赋值到args中
      var args = Array(startIndex + 1);
      for (index = 0; index < startIndex; index++) {
        args[index] = arguments[index];
      }
      // 对类似function  (a, b, c,...l)进行处理,args就类似于这样的形式[1, 2, 3, [4, 5, 6, 7]]
      args[startIndex] = rest;
      return func.apply(this, args);
    };
  };

在看完这个restArguments 函数的实现,我有这样的疑问,为什么要在switch中单独处理,startIndex等于0、1、2这三种情况呢

因为Function.prototype.call的效率要高于Function.prototype.apply 具体可查看这里

总结

防抖和节流,需要我们按照自己的需求,去选择合适的指定方式。在上述代码的基础上,可按照你的实际需要,进行相应的改造和继续完善。