资讯专栏INFORMATION COLUMN

throttle函数与debounce函数

Prasanta / 587人阅读

摘要:当函数被再次触发时,清除已设置的定时器,重新设置定时器。函数设置定时器,并根据传参配置决定是否在等待开始时执行函数。函数取消定时器,并重置内部参数。

throttle函数与debounce函数

有时候,我们会对一些触发频率较高的事件进行监听,如果在回调里执行高性能消耗的操作,反复触发时会使得性能消耗提高,浏览器卡顿,用户使用体验差。或者我们需要对触发的事件延迟执行回调,此时可以借助throttle/debounce函数来实现需求。

throttle函数

throttle函数用于限制函数触发的频率,每个delay时间间隔,最多只能执行函数一次。一个最常见的例子是在监听resize/scroll事件时,为了性能考虑,需要限制回调执行的频率,此时便会使用throttle函数进行限制。

由throttle函数的定义可知,每个delay时间间隔,最多只能执行函数一次,所以需要有一个变量来记录上一个执行函数的时刻,再结合延迟时间和当前触发函数的时刻来判断当前是否可以执行函数。在设定的时间间隔内,函数最多只能被执行一次。同时,第一次触发时立即执行函数。以下为throttle实现的简略代码:

function throttle(fn, delay) {
    var timer;
    return function() {
        var last = timer;
        var now = Date.now();
        if(!last) {
          timer = now;
          fn.apply(this,arguments);
          return;
        }
        if(last + delay > now) return;
        timer = now;
        fn.apply(this,arguments);
    }
}
debounce函数

debounce函数同样可以减少函数触发的频率,但限制的方式有点不同。当函数触发时,使用一个定时器延迟执行操作。当函数被再次触发时,清除已设置的定时器,重新设置定时器。如果上一次的延迟操作还未执行,则会被清除。一个最常见的业务场景是监听onchange事件,根据用户输入进行搜索,获取远程数据。为避免多次ajax请求,使用debounce函数作为onchange的回调。

由debounce的用途可知,实现延迟回调需要用到setTimeout设置定时器,每次重新触发时需要清除原来的定时器并重新设置,简单的代码实现如下:

function debounce(fn, delay){
    var timer;
    return function(){
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=>{
            timer = undefined
            fn.apply(this, arguments);
        }, delay||0)
    }
}
小结

throttle函数与debounce函数的区别就是throttle函数在触发后会马上执行,而debounce函数会在一定延迟后才执行。从触发开始到延迟结束,只执行函数一次。上文中throttle函数实现并未使用定时器,开源类库提供的throttle方法大多使用定时器实现,而且开源通过参数配置项,区分throttle函数与debounce函数。

实现throttle和debounce的开源库

上文中实现的代码较为简单,未考虑参数类型的判断及配置、测试等。下面介绍部分实现throttle和debounce的开源的类库。

jQuery.throttle jQuery.debounce

$.throttle指向函数jq_throttlejq_throttle接收四个参数 delay, no_trailing, callback, debounce_mode。参数二no_trailing在throttle模式中指示。除了在文档上说明的三个参数外,第四个参数debounce_mode用于指明是否是debounce模式,真即debounce模式,否则是throttle模式。

jq_throttle函数内,先声明需要使用的变量timeout_id(定时器)和last_exec(上一次执行操作的时间),进行了参数判断和交换,然后定义了内部函数wrapper,作为返回的函数。

wrapper内,有用于更新上次执行操作的时刻并执行真正的操作的函数exec,用于清除debounce模式中定时器的函数clear,保存当前触发时刻和上一次执行操作时刻的时间间隔的变量elapsed

如果是debounce模式且timeout_id空,执行exec。如果定时器timeout_id存在则清除定时器。

如果是throttle模式且elapsed大于延迟时间delay,执行exec;否则,当no_trainling非真时,更新timeout_id,重新设置定时器,补充在上面清除的定时器:如果是debounce模式,执行timeout_id = setTimeout(clear, delay),如果是throttle模式,执行timeout_id = setTimeout(exec, delay - elapsed)

 $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
    // After wrapper has stopped being called, this timeout ensures that
    // `callback` is executed at the proper times in `throttle` and `end`
    // debounce modes.
    var timeout_id,
      
      // Keep track of the last time `callback` was executed.
      last_exec = 0;
    
    // `no_trailing` defaults to falsy.
    if ( typeof no_trailing !== "boolean" ) {
      debounce_mode = callback;
      callback = no_trailing;
      no_trailing = undefined;
    }
    
    // The `wrapper` function encapsulates all of the throttling / debouncing
    // functionality and when executed will limit the rate at which `callback`
    // is executed.
    function wrapper() {
      var that = this,
        elapsed = +new Date() - last_exec,
        args = arguments;
      
      // Execute `callback` and update the `last_exec` timestamp.
      function exec() {
        last_exec = +new Date();
        callback.apply( that, args );
      };
      
      // If `debounce_mode` is true (at_begin) this is used to clear the flag
      // to allow future `callback` executions.
      function clear() {
        timeout_id = undefined;
      };
      
      if ( debounce_mode && !timeout_id ) {
        // Since `wrapper` is being called for the first time and
        // `debounce_mode` is true (at_begin), execute `callback`.
        exec();
      }
      
      // Clear any existing timeout.
      timeout_id && clearTimeout( timeout_id );
      
      if ( debounce_mode === undefined && elapsed > delay ) {
        // In throttle mode, if `delay` time has been exceeded, execute
        // `callback`.
        exec();
        
      } else if ( no_trailing !== true ) {
        // In trailing throttle mode, since `delay` time has not been
        // exceeded, schedule `callback` to execute `delay` ms after most
        // recent execution.
        // 
        // If `debounce_mode` is true (at_begin), schedule `clear` to execute
        // after `delay` ms.
        // 
        // If `debounce_mode` is false (at end), schedule `callback` to
        // execute after `delay` ms.
        timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
      }
    };
    
    // Set the guid of `wrapper` function to the same of original callback, so
    // it can be removed in jQuery 1.4+ .unbind or .die by using the original
    // callback as a reference.
    if ( $.guid ) {
      wrapper.guid = callback.guid = callback.guid || $.guid++;
    }
    
    // Return the wrapper function.
    return wrapper;
  };

debounce函数内部实际调用了throttle函数。

  $.debounce = function( delay, at_begin, callback ) {
    return callback === undefined
      ? jq_throttle( delay, at_begin, false )
      : jq_throttle( delay, callback, at_begin !== false );
  };
lodash的throttle与debounce

lodash中相比jQuery,提供了leadingtrailing选项,表示在函数在等待开始时被执行和函数在等待结束时被执行。而对于debounce函数,还提供了maxWait,当debounce函数重复触发时,有可能由于wait过长,回调函数没机会执行,maxWait字段确保了当函数重复触发时,每maxWait毫秒执行函数一次。

maxWait的作用,我们可以联想到,提供maxWait的debounce函数与throttle函数的作用是一样的;事实上,lodash的throttle函数就是指明maxWait的debounce函数。

lodash重新设置计时器时,并没有调用clearTimeout清除定时器,而是在执行回调前判断参数和执行上下文是否存在,存在时则执行回调,执行完之后将参数和上下文赋值为undefined;重复触发时,参数和上下文为空,不执行函数。这也是与jQuery实现的不同之一

以下为debounce函数内的函数和变量:

局部变量lastInvokeTime记录上次执行时间,默认0

函数invokeFunc执行回调操作,并更新上一次执行时间lastInvokeTime

函数leadingEdge设置定时器,并根据传参配置决定是否在等待开始时执行函数。

函数shouldInvoke判断是否可以执行回调函数。

函数timerExpired判断是否可以立即执行函数,如果可以则执行,否则重新设置定时器,函数remainingWait根据上次触发时间/执行时间和当前时间返回重新设置的定时器的时间间隔。

函数trailingEdge根据配置决定是否执行函数,并清空timerId

函数cancel取消定时器,并重置内部参数。函数debounced是返回的内部函数。

debounced内部先获取当前时间time,判断是否能执行函数。如果可以执行,且timerId空,表示可以马上执行函数(说明是第一次触发或已经执行过trailingEdge),执行leadingEdge,设置定时器。

如果timerId非空且传参选项有maxWait,说明是throttle函数,设置定时器延迟执行timerExpired并立即执行invokeFunc,此时在timerExpired中设置的定时器的延迟执行时间是wait - timeSinceLastCallmaxWait - timeSinceLastInvoke的最小值,分别表示通过wait设置的仍需等待执行函数的时间(下一次trailing的时间)和通过maxWait设置的仍需等待执行函数的时间(下一次maxing的时间)。

function debounce(func, wait, options) {
      var lastArgs,
          lastThis,
          maxWait,
          result,
          timerId,
          lastCallTime,
          lastInvokeTime = 0,
          leading = false,
          maxing = false,
          trailing = true;

      if (typeof func != "function") {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      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) {
        var args = lastArgs,
            thisArg = lastThis;

        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
      }

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

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

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

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

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

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

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

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }

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

throttle函数则是设置了maxWait选项且leading为真的debounce函数。

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
      });
    }
参考

Throttling and debouncing in JavaScript

Debouncing and Throttling Explained Through Examples

jquery-throttle-debounce源码

_.debounce源码

聊聊lodash的debounce实现

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

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

相关文章

  • 浅谈throttle以及debounce的原理和实现

    摘要:浅谈以及的原理和实现背景日常开发中我们经常会遇到一些需要节流调用或者压缩调用次数的情况例如之前我在完成一个需求的时候就遇到了因为后端并发问题导致收到多条信息从而导致函数被重复调用的情况当时的做法是通过对函数的调用进行注册遇到多次调用的时候清 浅谈throttle以及debounce的原理和实现 背景 日常开发中,我们经常会遇到一些需要节流调用,或者压缩调用次数的情况,例如之前我在完成...

    jsbintask 评论0 收藏0
  • JS throttledebounce的区别

    摘要:可以看下面的栗子这个图中图中每个小格大约,右边有原生事件与节流去抖插件的与事件。即如果有连续不断的触发,每执行一次,用在每隔一定间隔执行回调的场景。执行啦打印执行啦打印执行啦节流按照上面的说明,节流就是连续多次内的操作按照指定的间隔来执行。 一般在项目中我们会对input、scroll、resize等事件进行节流控制,防止事件过多触发,减少资源消耗;在vue的官网的例子中就有关于lod...

    wawor4827 评论0 收藏0
  • Debounce vs Throttle

    摘要:那么还有最后一个问题,那我之前设置的定时器怎么办呢定时器执行的是这个函数,而这个函数又会通过进行一次判断。 我们在处理事件的时候,有些事件由于触发太频繁,而每次事件都处理的话,会消耗太多资源,导致浏览器崩溃。最常见的是我们在移动端实现无限加载的时候,移动端本来滚动就不是很灵敏,如果每次滚动都处理的话,界面就直接卡死了。 因此,我们通常会选择,不立即处理事件,而是在触发一定次数或一定时间...

    xcold 评论0 收藏0
  • debouncing throttling

    摘要:一个使用场景某些浏览器事件可能会在短时间内高频触发,比如整窗口大小或滚动页面。这会导致非常严重的性能问题。实现与类似,接收两个参数,一个是需要截流的函数,另一个是函数执行间隔阈值。 一个使用场景:某些浏览器事件可能会在短时间内高频触发,比如:整窗口大小或滚动页面。如果给窗口滚动事件添加一个事件监听器,然后用户不停地快速滚动页面,那你的事件可能在短短数秒之内被触发数千次。这会导致非常严重...

    zzir 评论0 收藏0
  • [译]通过实例讲解Debouncing和Throtting(防抖节流)

    摘要:译通过实例讲解和防抖与节流源码中推荐的文章,为了学习英语,翻译了一下原文链接作者本文来自一位伦敦前端工程师的技术投稿。首次或立即你可能发现防抖事件在等待触发事件执行,直到事件都结束后它才执行。 [译]通过实例讲解Debouncing和Throtting(防抖与节流) lodash源码中推荐的文章,为了学习(英语),翻译了一下~ 原文链接 作者:DAVID CORBACHO 本文来自一位...

    Jenny_Tong 评论0 收藏0

发表评论

0条评论

Prasanta

|高级讲师

TA的文章

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