debounce.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. var isObject = require('../lang/isObject'),
  2. now = require('../date/now');
  3. /** Used as the `TypeError` message for "Functions" methods. */
  4. var FUNC_ERROR_TEXT = 'Expected a function';
  5. /* Native method references for those with the same name as other `lodash` methods. */
  6. var nativeMax = Math.max;
  7. /**
  8. * Creates a debounced function that delays invoking `func` until after `wait`
  9. * milliseconds have elapsed since the last time the debounced function was
  10. * invoked. The debounced function comes with a `cancel` method to cancel
  11. * delayed invocations. Provide an options object to indicate that `func`
  12. * should be invoked on the leading and/or trailing edge of the `wait` timeout.
  13. * Subsequent calls to the debounced function return the result of the last
  14. * `func` invocation.
  15. *
  16. * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked
  17. * on the trailing edge of the timeout only if the the debounced function is
  18. * invoked more than once during the `wait` timeout.
  19. *
  20. * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)
  21. * for details over the differences between `_.debounce` and `_.throttle`.
  22. *
  23. * @static
  24. * @memberOf _
  25. * @category Function
  26. * @param {Function} func The function to debounce.
  27. * @param {number} [wait=0] The number of milliseconds to delay.
  28. * @param {Object} [options] The options object.
  29. * @param {boolean} [options.leading=false] Specify invoking on the leading
  30. * edge of the timeout.
  31. * @param {number} [options.maxWait] The maximum time `func` is allowed to be
  32. * delayed before it's invoked.
  33. * @param {boolean} [options.trailing=true] Specify invoking on the trailing
  34. * edge of the timeout.
  35. * @returns {Function} Returns the new debounced function.
  36. * @example
  37. *
  38. * // avoid costly calculations while the window size is in flux
  39. * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
  40. *
  41. * // invoke `sendMail` when the click event is fired, debouncing subsequent calls
  42. * jQuery('#postbox').on('click', _.debounce(sendMail, 300, {
  43. * 'leading': true,
  44. * 'trailing': false
  45. * }));
  46. *
  47. * // ensure `batchLog` is invoked once after 1 second of debounced calls
  48. * var source = new EventSource('/stream');
  49. * jQuery(source).on('message', _.debounce(batchLog, 250, {
  50. * 'maxWait': 1000
  51. * }));
  52. *
  53. * // cancel a debounced call
  54. * var todoChanges = _.debounce(batchLog, 1000);
  55. * Object.observe(models.todo, todoChanges);
  56. *
  57. * Object.observe(models, function(changes) {
  58. * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) {
  59. * todoChanges.cancel();
  60. * }
  61. * }, ['delete']);
  62. *
  63. * // ...at some point `models.todo` is changed
  64. * models.todo.completed = true;
  65. *
  66. * // ...before 1 second has passed `models.todo` is deleted
  67. * // which cancels the debounced `todoChanges` call
  68. * delete models.todo;
  69. */
  70. function debounce(func, wait, options) {
  71. var args,
  72. maxTimeoutId,
  73. result,
  74. stamp,
  75. thisArg,
  76. timeoutId,
  77. trailingCall,
  78. lastCalled = 0,
  79. maxWait = false,
  80. trailing = true;
  81. if (typeof func != 'function') {
  82. throw new TypeError(FUNC_ERROR_TEXT);
  83. }
  84. wait = wait < 0 ? 0 : (+wait || 0);
  85. if (options === true) {
  86. var leading = true;
  87. trailing = false;
  88. } else if (isObject(options)) {
  89. leading = !!options.leading;
  90. maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait);
  91. trailing = 'trailing' in options ? !!options.trailing : trailing;
  92. }
  93. function cancel() {
  94. if (timeoutId) {
  95. clearTimeout(timeoutId);
  96. }
  97. if (maxTimeoutId) {
  98. clearTimeout(maxTimeoutId);
  99. }
  100. lastCalled = 0;
  101. maxTimeoutId = timeoutId = trailingCall = undefined;
  102. }
  103. function complete(isCalled, id) {
  104. if (id) {
  105. clearTimeout(id);
  106. }
  107. maxTimeoutId = timeoutId = trailingCall = undefined;
  108. if (isCalled) {
  109. lastCalled = now();
  110. result = func.apply(thisArg, args);
  111. if (!timeoutId && !maxTimeoutId) {
  112. args = thisArg = undefined;
  113. }
  114. }
  115. }
  116. function delayed() {
  117. var remaining = wait - (now() - stamp);
  118. if (remaining <= 0 || remaining > wait) {
  119. complete(trailingCall, maxTimeoutId);
  120. } else {
  121. timeoutId = setTimeout(delayed, remaining);
  122. }
  123. }
  124. function maxDelayed() {
  125. complete(trailing, timeoutId);
  126. }
  127. function debounced() {
  128. args = arguments;
  129. stamp = now();
  130. thisArg = this;
  131. trailingCall = trailing && (timeoutId || !leading);
  132. if (maxWait === false) {
  133. var leadingCall = leading && !timeoutId;
  134. } else {
  135. if (!maxTimeoutId && !leading) {
  136. lastCalled = stamp;
  137. }
  138. var remaining = maxWait - (stamp - lastCalled),
  139. isCalled = remaining <= 0 || remaining > maxWait;
  140. if (isCalled) {
  141. if (maxTimeoutId) {
  142. maxTimeoutId = clearTimeout(maxTimeoutId);
  143. }
  144. lastCalled = stamp;
  145. result = func.apply(thisArg, args);
  146. }
  147. else if (!maxTimeoutId) {
  148. maxTimeoutId = setTimeout(maxDelayed, remaining);
  149. }
  150. }
  151. if (isCalled && timeoutId) {
  152. timeoutId = clearTimeout(timeoutId);
  153. }
  154. else if (!timeoutId && wait !== maxWait) {
  155. timeoutId = setTimeout(delayed, wait);
  156. }
  157. if (leadingCall) {
  158. isCalled = true;
  159. result = func.apply(thisArg, args);
  160. }
  161. if (isCalled && !timeoutId && !maxTimeoutId) {
  162. args = thisArg = undefined;
  163. }
  164. return result;
  165. }
  166. debounced.cancel = cancel;
  167. return debounced;
  168. }
  169. module.exports = debounce;