debounce.js 5.7 KB

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