You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
5.4 KiB
JavaScript
182 lines
5.4 KiB
JavaScript
9 years ago
|
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;
|