资讯专栏INFORMATION COLUMN

Debounce vs Throttle

xcold / 2712人阅读

摘要:那么还有最后一个问题,那我之前设置的定时器怎么办呢定时器执行的是这个函数,而这个函数又会通过进行一次判断。

我们在处理事件的时候,有些事件由于触发太频繁,而每次事件都处理的话,会消耗太多资源,导致浏览器崩溃。最常见的是我们在移动端实现无限加载的时候,移动端本来滚动就不是很灵敏,如果每次滚动都处理的话,界面就直接卡死了。

因此,我们通常会选择,不立即处理事件,而是在触发一定次数或一定时间之后进行处理。这时候我们有两个选择: debounce(防抖动)和 throttle(节流阀)。

之前看过很多文章都还是没有太弄明白两者之间的区别,最后通过看源码大致了解了两者之间的区别以及简单的实现思路。

首先,我们通过实践来最简单的看看二者的区别:

可以看到,throttle会在第一次事件触发的时候就执行,然后每隔wait(我这里设置的2000ms)执行一次,而debounce只会在事件结束之后执行一次。

有了一个大概的印象之后,我们看一看lodash的源码对debouncethrottle的区别。

这里讨论默认情况

function throttle(func, wait, options) {
  let leading = true,
    trailing = true;

  if (typeof func !== "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (typeof options === "object") {
    leading = "leading" in options
      ? !!options.leading
      : leading;
    trailing = "trailing" in options
      ? !!options.trailing
      : trailing;
  }
  return debounce(func, wait, {
    leading,
    maxWait: wait,
    trailing,
  });
}

可以看到,throttle最后返回的还是debounce函数,只是指定了options选项。那么接下来我们就集中分析debounce

function debounce(fn, wait, options) {
    var lastArgs,
      lastThis,
          maxWait,
          result,
          timerId,
          lastCallTime,
          lastInvokeTime = 0,
          leading = false,
          maxing = false,
          trailing = true;
      function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);

        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        return result;
      }
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
}

为了记录每次执行的相关信息,debounce函数最后返回的是一个函数,形成一个闭包。

这也解释了为什么这样写不行:

    window.addEventListener("resize", function(){
      _.debounce(onResize, 2000);
    });

这样写根本就不会调用内部的debounced函数。

解决第一个不同

debounced内部呢,首先记录了当前调用的时间,然后通过shouldInvoke这个函数判断是否应该调用传入的func

      function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        // Either this is the first call, activity has stopped and we"re at the
        // trailing edge, the system time has gone backwards and we"re treating
        // it as the trailing edge, or we"ve hit the `maxWait` limit.
        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
      }

可以看到,该函数返回true的几个条件。其中需要我们引起注意的是最后一个条件,这是debouncethrottle的区别之一。

首先maxing通过函数开始的几行代码判断:

      if (isObject(options)) {
        leading = !!options.leading;
        maxing = "maxWait" in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = "trailing" in options ? !!options.trailing : trailing;
      }

我们看到,在定义throttle的时候, 给debounce函数给传入了options, 而里面包含maxWait这个属性,因此,对于throttle来说,maxingtrue, 而没有传入optionsdebounce则为false。这就是二者区别之一。在这里决定了shouldInvoke函数返回的值,以及是否执行接下去的逻辑判断。

我们再回到debounced这个函数:

  if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }

在第一次调用的时候,debouncethrottleisInvoking
true, 且此时timerId === undefined也成立,就返回leadingEdge(lastCallTime)这个函数。

那么我们再来看看leadingEdge 这个函数;

      function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time;
        // Start the timer for the trailing edge.
        timerId = setTimeout(timerExpired, wait);
        // Invoke the leading edge.
        return leading ? invokeFunc(time) : result;
      }

这里出现了debouncethrottle的第二个区别。这个函数首先是设置了一个定时器,随后返回的结果由leading决定。在默认情况下,throttle传入的leadingtrue,而debouncefalse。因此,throttle会马上执行传入的函数,而debounce不会。

这里我们就解决了它们的第一个不同:throttle会在第一次调用的时候就执行,而debounce不会。

解决第二个不同

我们再回到shouldInvoke的返回条件那里,如果在一个时间内频繁的调用, 前面三个条件都不会成立,对于debounce来说,最后一个也不会成立。而对于throttle来说,首先maxingtrue, 而如果距离上一次*传入的func 函数调用 大于maxWait最长等待时间的话,它也会返回true

function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
      }
        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }

那么在shouldInvoke成立之后,throttle会设置一个定时器,返回执行传入函数的结果。

这就是debouncethrottle 之间的第二个区别:throttle会保证你每隔一段时间都会执行,而debounce不会。

那么还有最后一个问题,那我之前设置的定时器怎么办呢?

timerId = setTimeout(timerExpired, wait);

定时器执行的是timerExpired这个函数,而这个函数又会通过shouldInvoke进行一次判断。

function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(timerExpired, remainingWait(time));
      }

最后,传入的func怎么执行的呢?下面这个函数实现:

function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;

        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
      }
饿了么的简单实现

在看饿了么的infinite scroll这个源码的时候,看到了一个简单版本的实现:

var throttle = function (fn, delay) {
  var now, lastExec, timer, context, args; 

  var execute = function () {
    fn.apply(context, args);
    lastExec = now;
  };

  return function () {
    context = this;
    args = arguments;

    now = Date.now();

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (lastExec) {
      var diff = delay - (now - lastExec);
      if (diff < 0) {
        execute();
      } else {
        timer = setTimeout(() => {
          execute();
        }, diff);
      }
    } else {
      execute();
    }
  };
};

那么它的思路很简单:

通过lastExec判断是否是第一次调用,如果是,就马上执行处理函数。

随后就会监测,每次调用的时间与上次执行函数的时间差,如果小于0,就立马执行。大于0就会在事件间隔之后执行。

每次调用的时候都会清除掉上一次的定时任务,这样就会保证只有一个最近的定时任务在等待执行。

那么它与lodash的一个最大的区别呢,就是它是关注与上次执行处理函数的时间差, 而lodashshouldInvoke关注的是两次事件调用函数的时间差

总结

总的来说,这种实现的主要部分呢,就是时间差定时器

最后,自己参照写了简单的debouncethrottle: Gist求指教!

参考资料

debouncing-throttling-explained-examples | CSS-Tricks

Lodash源码

饿了么 vue-infinite-scroll

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84048.html

相关文章

  • 一次发现underscore源码bug的经历以及对学术界拿来主义的思考

    摘要:事情是如何发生的最近干了件事情,发现了源码的一个。楼主找到的关于和区别的资料如下关于拿来主义为什么这么多文章里会出现泽卡斯的错误代码楼主想到了一个词,叫做拿来主义。的文章,就深刻抨击了拿来主义这一现象。 事情是如何发生的 最近干了件事情,发现了 underscore 源码的一个 bug。这件事本身并没有什么可说的,但是过程值得我们深思,记录如下,各位看官仁者见仁智者见智。 平时有浏览别...

    Lionad-Morotar 评论0 收藏0
  • throttledebounce的区别

    摘要:自己尝试一下年在的文章中第一次看到的实现方法。这三种实现方法内部不同,但是接口几乎一致。如你所见,我们使用了参数,因为我们只对用户停止改变浏览器大小时最后一次事件感兴趣。 前几天看到一篇文章,我的公众号里也分享了《一次发现underscore源码bug的经历以及对学术界拿来主义的思考》具体文章详见,微信公众号:showImg(https://segmentfault.com/img/b...

    Pluser 评论0 收藏0
  • 【译】通过例子解释 DebounceThrottle

    摘要:举例举例通过拖拽浏览器窗口,可以触发很多次事件。不支持,所以不能在服务端用于文件系统事件。总结将一系列迅速触发的事件例如敲击键盘合并成一个单独的事件。确保一个持续的操作流以每毫秒执行一次的速度执行。 Debounce 和 Throttle 是两个很相似但是又不同的技术,都可以控制一个函数在一段时间内执行的次数。 当我们在操作 DOM 事件的时候,为函数添加 debounce 或者 th...

    LeoHsiun 评论0 收藏0
  • JS进阶篇3---函数“节流” VS “防抖”

    摘要:目的都是为了降低回调函数执行频率,节省计算机资源,优化性能,提升用户体验。函数防抖事件频繁触发的情况下,只有经过足够的空闲时间,才执行代码一次。 函数节流和函数防抖的对比分析 一、前言 前端开发中,函数节流(throttle) 和 函数防抖(debounce) 作为常用的性能优化方法,两者都是用于优化高频率执行 js 代码的手段,那具体它们有什么异同点呢?有对这两个概念不太了解的小伙伴...

    hlcc 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<