debounce.js
  1  var isObject = require('./isObject'),
  2      now = require('./now'),
  3      toNumber = require('./toNumber');
  4  
  5  /** Error message constants. */
  6  var FUNC_ERROR_TEXT = 'Expected a function';
  7  
  8  /* Built-in method references for those with the same name as other `lodash` methods. */
  9  var nativeMax = Math.max,
 10      nativeMin = Math.min;
 11  
 12  /**
 13   * Creates a debounced function that delays invoking `func` until after `wait`
 14   * milliseconds have elapsed since the last time the debounced function was
 15   * invoked. The debounced function comes with a `cancel` method to cancel
 16   * delayed `func` invocations and a `flush` method to immediately invoke them.
 17   * Provide `options` to indicate whether `func` should be invoked on the
 18   * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
 19   * with the last arguments provided to the debounced function. Subsequent
 20   * calls to the debounced function return the result of the last `func`
 21   * invocation.
 22   *
 23   * **Note:** If `leading` and `trailing` options are `true`, `func` is
 24   * invoked on the trailing edge of the timeout only if the debounced function
 25   * is invoked more than once during the `wait` timeout.
 26   *
 27   * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 28   * until to the next tick, similar to `setTimeout` with a timeout of `0`.
 29   *
 30   * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 31   * for details over the differences between `_.debounce` and `_.throttle`.
 32   *
 33   * @static
 34   * @memberOf _
 35   * @since 0.1.0
 36   * @category Function
 37   * @param {Function} func The function to debounce.
 38   * @param {number} [wait=0] The number of milliseconds to delay.
 39   * @param {Object} [options={}] The options object.
 40   * @param {boolean} [options.leading=false]
 41   *  Specify invoking on the leading edge of the timeout.
 42   * @param {number} [options.maxWait]
 43   *  The maximum time `func` is allowed to be delayed before it's invoked.
 44   * @param {boolean} [options.trailing=true]
 45   *  Specify invoking on the trailing edge of the timeout.
 46   * @returns {Function} Returns the new debounced function.
 47   * @example
 48   *
 49   * // Avoid costly calculations while the window size is in flux.
 50   * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 51   *
 52   * // Invoke `sendMail` when clicked, debouncing subsequent calls.
 53   * jQuery(element).on('click', _.debounce(sendMail, 300, {
 54   *   'leading': true,
 55   *   'trailing': false
 56   * }));
 57   *
 58   * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
 59   * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
 60   * var source = new EventSource('/stream');
 61   * jQuery(source).on('message', debounced);
 62   *
 63   * // Cancel the trailing debounced invocation.
 64   * jQuery(window).on('popstate', debounced.cancel);
 65   */
 66  function debounce(func, wait, options) {
 67    var lastArgs,
 68        lastThis,
 69        maxWait,
 70        result,
 71        timerId,
 72        lastCallTime,
 73        lastInvokeTime = 0,
 74        leading = false,
 75        maxing = false,
 76        trailing = true;
 77  
 78    if (typeof func != 'function') {
 79      throw new TypeError(FUNC_ERROR_TEXT);
 80    }
 81    wait = toNumber(wait) || 0;
 82    if (isObject(options)) {
 83      leading = !!options.leading;
 84      maxing = 'maxWait' in options;
 85      maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
 86      trailing = 'trailing' in options ? !!options.trailing : trailing;
 87    }
 88  
 89    function invokeFunc(time) {
 90      var args = lastArgs,
 91          thisArg = lastThis;
 92  
 93      lastArgs = lastThis = undefined;
 94      lastInvokeTime = time;
 95      result = func.apply(thisArg, args);
 96      return result;
 97    }
 98  
 99    function leadingEdge(time) {
100      // Reset any `maxWait` timer.
101      lastInvokeTime = time;
102      // Start the timer for the trailing edge.
103      timerId = setTimeout(timerExpired, wait);
104      // Invoke the leading edge.
105      return leading ? invokeFunc(time) : result;
106    }
107  
108    function remainingWait(time) {
109      var timeSinceLastCall = time - lastCallTime,
110          timeSinceLastInvoke = time - lastInvokeTime,
111          timeWaiting = wait - timeSinceLastCall;
112  
113      return maxing
114        ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
115        : timeWaiting;
116    }
117  
118    function shouldInvoke(time) {
119      var timeSinceLastCall = time - lastCallTime,
120          timeSinceLastInvoke = time - lastInvokeTime;
121  
122      // Either this is the first call, activity has stopped and we're at the
123      // trailing edge, the system time has gone backwards and we're treating
124      // it as the trailing edge, or we've hit the `maxWait` limit.
125      return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
126        (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
127    }
128  
129    function timerExpired() {
130      var time = now();
131      if (shouldInvoke(time)) {
132        return trailingEdge(time);
133      }
134      // Restart the timer.
135      timerId = setTimeout(timerExpired, remainingWait(time));
136    }
137  
138    function trailingEdge(time) {
139      timerId = undefined;
140  
141      // Only invoke if we have `lastArgs` which means `func` has been
142      // debounced at least once.
143      if (trailing && lastArgs) {
144        return invokeFunc(time);
145      }
146      lastArgs = lastThis = undefined;
147      return result;
148    }
149  
150    function cancel() {
151      if (timerId !== undefined) {
152        clearTimeout(timerId);
153      }
154      lastInvokeTime = 0;
155      lastArgs = lastCallTime = lastThis = timerId = undefined;
156    }
157  
158    function flush() {
159      return timerId === undefined ? result : trailingEdge(now());
160    }
161  
162    function debounced() {
163      var time = now(),
164          isInvoking = shouldInvoke(time);
165  
166      lastArgs = arguments;
167      lastThis = this;
168      lastCallTime = time;
169  
170      if (isInvoking) {
171        if (timerId === undefined) {
172          return leadingEdge(lastCallTime);
173        }
174        if (maxing) {
175          // Handle invocations in a tight loop.
176          clearTimeout(timerId);
177          timerId = setTimeout(timerExpired, wait);
178          return invokeFunc(lastCallTime);
179        }
180      }
181      if (timerId === undefined) {
182        timerId = setTimeout(timerExpired, wait);
183      }
184      return result;
185    }
186    debounced.cancel = cancel;
187    debounced.flush = flush;
188    return debounced;
189  }
190  
191  module.exports = debounce;