Add a cancel function to debounced functions. [closes #567]

This commit is contained in:
John-David Dalton
2014-05-24 14:39:53 -07:00
parent 0c1c4b08c7
commit 1ce0fffd25
2 changed files with 73 additions and 19 deletions

View File

@@ -5363,6 +5363,7 @@
/** /**
* Creates a function that will delay the execution of `func` until after * Creates a function that will delay the execution of `func` until after
* `wait` milliseconds have elapsed since the last time it was invoked. * `wait` milliseconds have elapsed since the last time it was invoked.
* The created function comes with a `cancel` method to cancel delayed calls.
* Provide an options object to indicate that `func` should be invoked on * Provide an options object to indicate that `func` should be invoked on
* the leading and/or trailing edge of the `wait` timeout. Subsequent calls * the leading and/or trailing edge of the `wait` timeout. Subsequent calls
* to the debounced function will return the result of the last `func` call. * to the debounced function will return the result of the last `func` call.
@@ -5384,8 +5385,7 @@
* @example * @example
* *
* // avoid costly calculations while the window size is in flux * // avoid costly calculations while the window size is in flux
* var lazyLayout = _.debounce(calculateLayout, 150); * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
* jQuery(window).on('resize', lazyLayout);
* *
* // execute `sendMail` when the click event is fired, debouncing subsequent calls * // execute `sendMail` when the click event is fired, debouncing subsequent calls
* jQuery('#postbox').on('click', _.debounce(sendMail, 300, { * jQuery('#postbox').on('click', _.debounce(sendMail, 300, {
@@ -5395,9 +5395,26 @@
* *
* // ensure `batchLog` is executed once after 1 second of debounced calls * // ensure `batchLog` is executed once after 1 second of debounced calls
* var source = new EventSource('/stream'); * var source = new EventSource('/stream');
* source.addEventListener('message', _.debounce(batchLog, 250, { * jQuery(source).on('message', _.debounce(batchLog, 250, {
* 'maxWait': 1000 * 'maxWait': 1000
* }, false); * }, false);
*
* // cancelling a debounced call
* var todoChanges = _.debounce(batchLog, 1000);
* Object.observe(models.todo, todoChanges);
*
* Object.observe(models, function(changes) {
* if (_.find(changes, { 'name': '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) { function debounce(func, wait, options) {
var args, var args,
@@ -5423,7 +5440,18 @@
maxWait = 'maxWait' in options && nativeMax(wait, +options.maxWait || 0); maxWait = 'maxWait' in options && nativeMax(wait, +options.maxWait || 0);
trailing = 'trailing' in options ? options.trailing : trailing; trailing = 'trailing' in options ? options.trailing : trailing;
} }
var delayed = function() {
function cancel() {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (maxTimeoutId) {
clearTimeout(maxTimeoutId);
}
maxTimeoutId = timeoutId = trailingCall = undefined;
}
function delayed() {
var remaining = wait - (now() - stamp); var remaining = wait - (now() - stamp);
if (remaining <= 0 || remaining > wait) { if (remaining <= 0 || remaining > wait) {
if (maxTimeoutId) { if (maxTimeoutId) {
@@ -5441,9 +5469,9 @@
} else { } else {
timeoutId = setTimeout(delayed, remaining); timeoutId = setTimeout(delayed, remaining);
} }
}; }
var maxDelayed = function() { function maxDelayed() {
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
@@ -5455,9 +5483,9 @@
args = thisArg = null; args = thisArg = null;
} }
} }
}; }
return function() { function debounced() {
args = arguments; args = arguments;
stamp = now(); stamp = now();
thisArg = this; thisArg = this;
@@ -5497,7 +5525,9 @@
args = thisArg = null; args = thisArg = null;
} }
return result; return result;
}; }
debounced.cancel = cancel;
return debounced;
} }
/** /**
@@ -5732,11 +5762,12 @@
} }
/** /**
* Creates a function that, when executed, will only call the `func` function * Creates a function that will only call the `func` function at most once
* at most once per every `wait` milliseconds. Provide an options object to * per every `wait` milliseconds. The created function comes with a `cancel`
* indicate that `func` should be invoked on the leading and/or trailing edge * method to cancel delayed calls. Provide an options object to indicate that
* of the `wait` timeout. Subsequent calls to the throttled function will * `func` should be invoked on the leading and/or trailing edge of the `wait`
* return the result of the last `func` call. * timeout. Subsequent calls to the throttled function will return the result
* of the last `func` call.
* *
* Note: If `leading` and `trailing` options are `true`, `func` will be called * Note: If `leading` and `trailing` options are `true`, `func` will be called
* on the trailing edge of the timeout only if the the throttled function is * on the trailing edge of the timeout only if the the throttled function is
@@ -5754,13 +5785,14 @@
* @example * @example
* *
* // avoid excessively updating the position while scrolling * // avoid excessively updating the position while scrolling
* var throttled = _.throttle(updatePosition, 100); * jQuery(window).on('scroll', _.throttle(updatePosition, 100));
* jQuery(window).on('scroll', throttled);
* *
* // execute `renewToken` when the click event is fired, but not more than once every 5 minutes * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes
* jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { * var throttled = _.throttle(renewToken, 300000, { 'trailing': false })
* 'trailing': false * jQuery('.interactive').on('click',);
* })); *
* // cancelling a trailing throttled call
* jQuery(window).on('popstate', throttled.cancel);
*/ */
function throttle(func, wait, options) { function throttle(func, wait, options) {
var leading = true, var leading = true,

View File

@@ -9412,6 +9412,28 @@
QUnit.start(); QUnit.start();
} }
}); });
asyncTest('_.' + methodName + ' should support cancelling delayed calls', 1, function() {
if (!(isRhino && isModularize)) {
var callCount = 0;
var funced = func(function() {
callCount++;
}, 32, { 'leading': false });
funced();
funced.cancel();
setTimeout(function() {
strictEqual(callCount, 0);
QUnit.start();
}, 64);
}
else {
skipTest();
QUnit.start();
}
});
}); });
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/