资讯专栏INFORMATION COLUMN

从lodash源码学习节流与防抖

CloudDeveloper / 744人阅读

摘要:首先重置防抖函数最后调用时间,然后去触发一个定时器,保证后接下来的执行。这就避免了手动管理定时器。

  之前遇到过一个场景,页面上有几个d3.js绘制的图形。如果调整浏览器可视区大小,会引发图形重绘。当图中的节点比较多的时候,页面会显得异常卡顿。为了限制类似于这种短时间内高频率触发的情况,我们可以使用防抖函数。

  实际开发过程中,这样的情况其实很多,比如:

页面的scroll事件

input框等的输入事件

拖拽事件用到的mousemove等

  先说说防抖和节流是个啥,有啥区别

防抖:设定一个时间间隔,当某个频繁触发的函数执行一次后,在这个时间间隔内不会再次被触发,如果在此期间尝试触发这个函数,则时间间隔会重新开始计算。

节流:设定一个时间间隔,某个频繁触发的函数,在这个时间间隔内只会执行一次。也就是说,这个频繁触发的函数会以一个固定的周期执行。

debounce(函数防抖)

  大致捋一遍代码结构。为了方便阅读,我们先把源码中的Function注释掉。

function debounce(func, wait, options) {
    // 代码一开始,以闭包的形式定义了一些变量
      var lastArgs,  //  最后一次debounce的arguments,它其实起一个标记位的作用,后面会提到
          lastThis,  //  就是last this,用来修正this指向
          maxWait,   //  存储option里面传入的maxWait值,最大等待时间
          result,    //  其实这个result始终都是undefined
          timerId,   // setTimeout赋给它,用于表示当前定时器
          lastCallTime,   // 最后一次调用debounce的时刻
          lastInvokeTime = 0,    //  最后一次调用用户传入函数的时刻
          leading = false,   //  是否在一开始就执行用户传入的函数
          maxing = false,    //  是否有最大等待时间
          trailing = true;   //  是否在等待周期结束后执行用户传入的函数

    //  用户传入的fun必须是个函数,否则报错
      if (typeof func != "function") {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      
    //  toNumber是lodash封装的一个转类型的方法
      wait = toNumber(wait) || 0;
      
    //  获取用户传入的配置
      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;
      }

    //  执行用户传入的函数
      function invokeFunc(time) {
        // ......
      }

    //  防抖开始时执行的操作
      function leadingEdge(time) {)
        // ......
      }

    //  计算仍然需要等待的时间
      function remainingWait(time) {
        // ......
      }

    //  判断此时是否应该执行用户传入的函数
      function shouldInvoke(time) {
        // ......
      }

    //  等待时间结束后的操作
      function timerExpired() {
        // ......
      }

    //  执行用户传入的函数
      function trailingEdge(time) {
        // ......
      }

    //  取消防抖
      function cancel() {
        // ......
      }

     //  立即执行用户传入的函数
      function flush() {
        // ......
      }

    // 防抖开始的入口
      function debounced() {
        // ......
      }
      
      
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
    }

我们先从入口函数开始。函数开始执行后,首先会出现三种情况:

时间上达到了可以执行的条件;

时间上不满足条件,但是此时的定时器并没有启动;

不满足条件,返回undefined

  说实话,第二种情况没想到场景,哪位大佬给补充一下呢。

  代码中timerId = setTimeout(timerExpired, wait);是用来设置定时器,到时间后触发trailingEdge这个函数。

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 undefined
        return result;
      }

  我们先来看看shouldInvoke是如何判断函数是否可以执行的。

      function shouldInvoke(time) {
        //  lastCallTime初始值是undefined,lastInvokeTime初始值是0,
        //  防抖函数被手动取消后,这两个值会被设为初始值
        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) ||  //  上次调用时刻距离现在已经大于wait值
                (timeSinceLastCall < 0) ||      //  当前时间-上次调用时间小于0,应该只可能是手动修改了系统时间吧
                (maxing && timeSinceLastInvoke >= maxWait)  //  设置了最大等待时间,且已超时
            );
      }

  我们继续分析函数开始的阶段leadingEdge。首先重置防抖函数最后调用时间,然后去触发一个定时器,保证wait后接下来的执行。最后判断如果leadingtrue的话,立即执行用户传入的函数:

      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;
      }

  我们已经不止一次去设定触发器了,来我们探究一下里面到底做了啥。其实很简单,判断时间是否符合执行条件,符合的话触发trailingEdge,也就是后续操作,否则计算需要等待的时间,并重新调用这个函数,其实这里就是防抖的核心所在了。

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

  至于如何重新计算剩余时间的,这里不作过多解释,大家一看便知。

      function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting;
      }

  我们说说等待时间到了以后的操作。重置了一些本周期的变量。并且,如果trailingtrue而且lastArgs存在时,才会再次执行用户传入的参数。这里解释了文章开头提到的lastArgs只是个标记位,如注释所说,他表示debounce至少执行了一次。

      function trailingEdge(time) {
        timerId = undefined;

        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
      }

  执行用户传入的函数比较简单,我们知道callapply是会立即执行的,其实最后的result还是undefined

    function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;
        //  重置了一些条件
        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        //  执行用户传入函数
        result = func.apply(thisArg, args);
        return result;
      }

  最后就是取消防抖和立即执行用户传入函数的过程了,代码一目了然,不作过多解释。

      function cancel() {
        if (timerId !== undefined) {
          clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
      }

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }
throttle(函数节流)

  节流其实原理跟防抖是一样的,只不过触发条件不同而已,其实就是maxWaitwait的防抖函数。

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

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

  我们发现,其实lodash除了在cancle函数中使用了清除定时器的操作外,其他地方并没有去关心定时器,而是很巧妙的在定时器里加了一个判断条件来判断后续函数是否可以执行。这就避免了手动管理定时器。

  lodash替我们考虑到了一些比较少见的情景,而且还有一定的容错性。即便ES6实现了很多目前常用的工具函数,但是面对复杂的情景,我们依然可以以按需引入的方式使用lodash的一些函数来提升开发效率,同时使得我们的程序更加健壮。

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

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

相关文章

  • 浅谈节流防抖

    摘要:节流和防抖在开发项目过程中很常见,例如输入实时搜索滚动更新了,等等,大量的场景需要我们对其进行处理。防抖多次触发,只在最后一次触发时,执行目标函数。节流限制目标函数调用的频率,比如内不能调用次。 节流和防抖在开发项目过程中很常见,例如 input 输入实时搜索、scrollview 滚动更新了,等等,大量的场景需要我们对其进行处理。我们由 Lodash 来介绍,直接进入主题吧。 Lod...

    thursday 评论0 收藏0
  • 源码分析】给你几个闹钟,或许用 10 分钟就能写出 lodash 中的 debounce &

    摘要:最简单的案例以最简单的情景为例在某一时刻点只调用一次函数,那么将在时间后才会真正触发函数。后续我们会逐渐增加黑色闹钟出现的复杂度,不断去分析红色闹钟的位置。 序 相比网上教程中的 debounce 函数,lodash 中的 debounce 功能更为强大,相应的理解起来更为复杂; 解读源码一般都是直接拿官方源码来解读,不过这次我们采用另外的方式:从最简单的场景开始写代码,然后慢慢往源码...

    余学文 评论0 收藏0
  • 理解节流防抖

    摘要:节流节流限制了一个函数可以在短时间内被调用的次数。更新防抖防抖确保了一个函数只有在一个固定时间段内没有被调用过后,才会再次被调用。再换句话说防抖会等待事件不再高频发生,再触发。这个网站很好的可视化了节流与防抖。 节流 Throttling 节流限制了一个函数可以在短时间内被调用的次数。可以这样形容:在一毫秒内最多执行此函数 1 次。 Throttling enforces a maxi...

    glumes 评论0 收藏0
  • JavaScript之节流防抖

    摘要:个人博客原文地址背景我们在开发的过程中会经常使用如等事件,如果正常绑定事件处理函数的话,有可能在很短的时间内多次连续触发事件,十分影响性能。 个人博客原文地址 背景 我们在开发的过程中会经常使用如scroll、resize、touchmove等事件,如果正常绑定事件处理函数的话,有可能在很短的时间内多次连续触发事件,十分影响性能。因此针对这类事件要进行节流或者防抖处理 节流 节流的意思...

    wayneli 评论0 收藏0
  • 函数防抖(debounce)和节流(throttle)以及lodash的debounce源码赏析

    摘要:防抖函数防抖和节流是一对常常被放在一起的场景。同时,这里会设置一个定时器,在等待后会执行,的主要作用就是触发。最后,如果不再有函数调用,就会在定时器结束时执行。 函数节流和去抖的出现场景,一般都伴随着客户端 DOM 的事件监听。比如scroll resize等事件,这些事件在某些场景触发非常频繁。 比如,实现一个原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路监听...

    Enlightenment 评论0 收藏0

发表评论

0条评论

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