| var isObject = require('../lang/isObject'), |
| now = require('../date/now'); |
| |
| /** Used as the `TypeError` message for "Functions" methods. */ |
| var FUNC_ERROR_TEXT = 'Expected a function'; |
| |
| /* Native method references for those with the same name as other `lodash` methods. */ |
| var nativeMax = Math.max; |
| |
| /** |
| * Creates a debounced function that delays invoking `func` until after `wait` |
| * milliseconds have elapsed since the last time the debounced function was |
| * invoked. The debounced function comes with a `cancel` method to cancel |
| * delayed invocations. Provide an options object to indicate that `func` |
| * should be invoked on the leading and/or trailing edge of the `wait` timeout. |
| * Subsequent calls to the debounced function return the result of the last |
| * `func` invocation. |
| * |
| * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked |
| * on the trailing edge of the timeout only if the the debounced function is |
| * invoked more than once during the `wait` timeout. |
| * |
| * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) |
| * for details over the differences between `_.debounce` and `_.throttle`. |
| * |
| * @static |
| * @memberOf _ |
| * @category Function |
| * @param {Function} func The function to debounce. |
| * @param {number} [wait=0] The number of milliseconds to delay. |
| * @param {Object} [options] The options object. |
| * @param {boolean} [options.leading=false] Specify invoking on the leading |
| * edge of the timeout. |
| * @param {number} [options.maxWait] The maximum time `func` is allowed to be |
| * delayed before it's invoked. |
| * @param {boolean} [options.trailing=true] Specify invoking on the trailing |
| * edge of the timeout. |
| * @returns {Function} Returns the new debounced function. |
| * @example |
| * |
| * // avoid costly calculations while the window size is in flux |
| * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); |
| * |
| * // invoke `sendMail` when the click event is fired, debouncing subsequent calls |
| * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { |
| * 'leading': true, |
| * 'trailing': false |
| * })); |
| * |
| * // ensure `batchLog` is invoked once after 1 second of debounced calls |
| * var source = new EventSource('/stream'); |
| * jQuery(source).on('message', _.debounce(batchLog, 250, { |
| * 'maxWait': 1000 |
| * })); |
| * |
| * // cancel a debounced call |
| * var todoChanges = _.debounce(batchLog, 1000); |
| * Object.observe(models.todo, todoChanges); |
| * |
| * Object.observe(models, function(changes) { |
| * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { |
| * todoChanges.cancel(); |
| * } |
| * }, ['delete']); |
| * |
| * // ...at some point `models.todo` is changed |
| * models.todo.completed = true; |
| * |
| * // ...before 1 second has passed `models.todo` is deleted |
| * // which cancels the debounced `todoChanges` call |
| * delete models.todo; |
| */ |
| function debounce(func, wait, options) { |
| var args, |
| maxTimeoutId, |
| result, |
| stamp, |
| thisArg, |
| timeoutId, |
| trailingCall, |
| lastCalled = 0, |
| maxWait = false, |
| trailing = true; |
| |
| if (typeof func != 'function') { |
| throw new TypeError(FUNC_ERROR_TEXT); |
| } |
| wait = wait < 0 ? 0 : (+wait || 0); |
| if (options === true) { |
| var leading = true; |
| trailing = false; |
| } else if (isObject(options)) { |
| leading = !!options.leading; |
| maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); |
| trailing = 'trailing' in options ? !!options.trailing : trailing; |
| } |
| |
| function cancel() { |
| if (timeoutId) { |
| clearTimeout(timeoutId); |
| } |
| if (maxTimeoutId) { |
| clearTimeout(maxTimeoutId); |
| } |
| lastCalled = 0; |
| maxTimeoutId = timeoutId = trailingCall = undefined; |
| } |
| |
| function complete(isCalled, id) { |
| if (id) { |
| clearTimeout(id); |
| } |
| maxTimeoutId = timeoutId = trailingCall = undefined; |
| if (isCalled) { |
| lastCalled = now(); |
| result = func.apply(thisArg, args); |
| if (!timeoutId && !maxTimeoutId) { |
| args = thisArg = undefined; |
| } |
| } |
| } |
| |
| function delayed() { |
| var remaining = wait - (now() - stamp); |
| if (remaining <= 0 || remaining > wait) { |
| complete(trailingCall, maxTimeoutId); |
| } else { |
| timeoutId = setTimeout(delayed, remaining); |
| } |
| } |
| |
| function maxDelayed() { |
| complete(trailing, timeoutId); |
| } |
| |
| function debounced() { |
| args = arguments; |
| stamp = now(); |
| thisArg = this; |
| trailingCall = trailing && (timeoutId || !leading); |
| |
| if (maxWait === false) { |
| var leadingCall = leading && !timeoutId; |
| } else { |
| if (!maxTimeoutId && !leading) { |
| lastCalled = stamp; |
| } |
| var remaining = maxWait - (stamp - lastCalled), |
| isCalled = remaining <= 0 || remaining > maxWait; |
| |
| if (isCalled) { |
| if (maxTimeoutId) { |
| maxTimeoutId = clearTimeout(maxTimeoutId); |
| } |
| lastCalled = stamp; |
| result = func.apply(thisArg, args); |
| } |
| else if (!maxTimeoutId) { |
| maxTimeoutId = setTimeout(maxDelayed, remaining); |
| } |
| } |
| if (isCalled && timeoutId) { |
| timeoutId = clearTimeout(timeoutId); |
| } |
| else if (!timeoutId && wait !== maxWait) { |
| timeoutId = setTimeout(delayed, wait); |
| } |
| if (leadingCall) { |
| isCalled = true; |
| result = func.apply(thisArg, args); |
| } |
| if (isCalled && !timeoutId && !maxTimeoutId) { |
| args = thisArg = undefined; |
| } |
| return result; |
| } |
| debounced.cancel = cancel; |
| return debounced; |
| } |
| |
| module.exports = debounce; |