防抖和节流的代码分析

1,080 阅读6分钟
原文链接: blog.5udou.cn

前言

防抖和节流这两个概念在入门JavaScript的时候就遇到过,而之后的项目中没有真正的实践过,偶尔的一次机会终于让我使用到了这两个概念,于是顺便搞懂这二者的区别。

1、基本概念

二者顾名思义,防抖(debounce)的含义便是为了防止抖动造成的结果不准确,我们在抖动的过程中不去关注其中的变化,而是等到稳定的时候再处理结果。这种概念在硬件上一些电流或者电磁波图有着很多的应用。在电流中一般会有毛刺,而我们在计算结果的时候是不会去考虑这段异常的抖动,而是从整体上来评测结果,而在软件上来实现防抖,便是在抖动的过程中不去执行对应的动作,而是等到抖动结束趋于稳定的时候再来执行动作。

而节流(throttle)则是可以形象地描述为人为地设置一个闸口,让某一段时间内发生的时间的频率降低下来,这个频率可以由你决定。想象一下你在一条流动的小溪中设置了一个关卡,本来一小时流量有10立方米,但是因为你的节流导致流量变成了5立方米,这样我们就成为节流。

上面的解释估计不够形象,那么我们结合下面的demo更加深入的阐述。lodash源码中已经有实现这二者的功能。完整的demo如下(借鉴了其代码):

下面还有一张图片阐述了二者的区别:

(图片引自User Interfaces | Meteor Guide)

2、 防抖

首先我们看一下防抖的代码实现:

 const _now = Date.now || function () {
  return new Date().getTime();
}

// 函数去抖(连续事件触发结束后只触发一次)
// sample 1: debounce(function(){}, 1000)
// 连续事件结束后的 1000ms 后触发
// sample 1: debounce(function(){}, 1000, true)
// 连续事件触发后立即触发(此时会忽略第二个参数)
/* eslint-disable */
const debounce = function (func, wait, immediate) {
  let timeout, args, context, timestamp, result;

  const later = function () {
    const last = _now() - timestamp;

    if (last < wait && last >= 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function () {
    context = this;
    args = arguments;
    timestamp = _now();
    const callNow = immediate && !timeout;
    if (!timeout) {
      timeout = setTimeout(later, wait);
    }
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
};

整个代码实现的思路便是:

  1. 当你第一次点击debounce那个按钮的时候,执行return回来的函数,这个时候timeout等于undefined,如果你没有设置immediate为true的话,于是在这个时候就埋入了一个超时函数,并且timeout变成有定义的值了。但是如果immediate为true的话就会马上执行回调函数。
  2. 当你之后在wait的时间段内连续点击,这个时候因为JS的闭包效应,此时的timeout一直是有值的并且timestamp这个值是一直会更新,所以下面这两个if语句都不会进去:
     const callNow = immediate && !timeout;
     if (!timeout) {
       timeout = setTimeout(later, wait);
     }
     if (callNow) {
       result = func.apply(context, args);
       context = args = null;
     }
    
  3. 接着wait定时的时间到了之后,执行later函数,首先判断当前时间与最开始第一次点击的时间的差值是否还是小于wait时间,如果是说明还是有用户一直在点击,于是继续设置超时函数。
  4. 如果用户停止点击了,那么这个时候设置timeout为null,然后判断immediate是否为true,如果不是的话那么就可以执行回调函数了。

整个执行流程可以总结如下:

  1. immediate为true的时候,连续事件触发的时候只会在第一次触发的时候执行回调函数,后面的触发全都忽略掉。
  2. immediate为false的时候,连续事件触发的时候只会在最后一次触发后的wait事件后执行。

这么解释,各位看官不知道是否已经掌握了这个概念呢?是否真正明白了防抖的本质了呢?

说完防抖,接下来说说节流。

3、节流

节流的代码实现如下:

 const _now = Date.now || function () {
  return new Date().getTime();
 }
 const throttle = function (func, wait, options = {}) {
  let context, args, result;
  let timeout = null;
  let previous = 0;

  const later = function () {
    previous = options.leading === false ? 0 : _now();
    timeout = null;
    result = func.apply(context, args);

    if (!timeout) context = args = null;
  };

  return function () {
    // 记录当前时间戳
    const now = _now();

    if (!previous && options.leading === false) previous = now;

    const remaining = wait - (now - previous);
    context = this;
    args = arguments;

    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        // 解除引用,防止内存泄露
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) { // 最后一次需要触发的情况
      timeout = setTimeout(later, remaining);
    }
    // 回调返回值
    return result;
  };
}

节流的大致含义之前我们已经说过,这段代码实现的便是能够在固定的时间稳定地调用回调函数,而不会受用户的点击次数。实现思路如下:

  1. 用户第一次点击throttle按钮,执行return回来的函数,假设option没有配置。那么第一次下面这个if语句是成立的:
    if (remaining <= 0 || remaining > wait) {
       if (timeout) {
         clearTimeout(timeout);
         // 解除引用,防止内存泄露
         timeout = null;
       }
       previous = now;
       result = func.apply(context, args);
       if (!timeout) context = args = null;
     }
    
    于是在第一次进去的时候执行了回调函数,并且previous等于当前执行的时间
  2. 后面的连续点击会先判断每次点击距离最先开始执行的那次点击的时间点,然后再与wait进行判断,如果已经超过wait的时间,那么又可以执行新的一次回调函数了。
  3. 如果还没有超过,那么进入第二个if语句:
if (!timeout && options.trailing !== false)

于是埋入一个超时函数并且超时时间等于剩余的时间,这个时候用户再不断地点击按钮都没有用了。

  1. 超时函数定时到之后便可以执行回调函数了。
  2. 如果在超时时间还没到的时候,remaining满足if条件,那么依然会执行回调函数并将之前的超时函数的引用清掉。

整个执行流程可以总结为:

  1. 第一次触发事件执行回调函数,并以这个时间点为基点
  2. 然后每隔wait时间执行一次回调函数,而不管用户触发多少次事件。
  3. 用户停止点击后,最后一次触发一次后就清掉定时器了。

说了这么多,如果仔细阅读的童鞋必定能够完全掌握这两个概念的。这两段代码很容易在平时的面试中遇到哦~~~

最后的最后,祝大家端午节快乐!

4、参考

  1. User Interfaces