From 1ce0fffd25cfdf877dea80848202a98102bc0492 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 24 May 2014 14:39:53 -0700 Subject: [PATCH] Add a `cancel` function to `debounced` functions. [closes #567] --- lodash.js | 70 ++++++++++++++++++++++++++++++++++++++-------------- test/test.js | 22 +++++++++++++++++ 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/lodash.js b/lodash.js index a2547a287..50400cec0 100644 --- a/lodash.js +++ b/lodash.js @@ -5363,6 +5363,7 @@ /** * Creates a function that will delay the execution of `func` until after * `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 * 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. @@ -5384,8 +5385,7 @@ * @example * * // avoid costly calculations while the window size is in flux - * var lazyLayout = _.debounce(calculateLayout, 150); - * jQuery(window).on('resize', lazyLayout); + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); * * // execute `sendMail` when the click event is fired, debouncing subsequent calls * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { @@ -5395,9 +5395,26 @@ * * // ensure `batchLog` is executed once after 1 second of debounced calls * var source = new EventSource('/stream'); - * source.addEventListener('message', _.debounce(batchLog, 250, { + * jQuery(source).on('message', _.debounce(batchLog, 250, { * 'maxWait': 1000 * }, 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) { var args, @@ -5423,7 +5440,18 @@ maxWait = 'maxWait' in options && nativeMax(wait, +options.maxWait || 0); 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); if (remaining <= 0 || remaining > wait) { if (maxTimeoutId) { @@ -5441,9 +5469,9 @@ } else { timeoutId = setTimeout(delayed, remaining); } - }; + } - var maxDelayed = function() { + function maxDelayed() { if (timeoutId) { clearTimeout(timeoutId); } @@ -5455,9 +5483,9 @@ args = thisArg = null; } } - }; + } - return function() { + function debounced() { args = arguments; stamp = now(); thisArg = this; @@ -5497,7 +5525,9 @@ args = thisArg = null; } return result; - }; + } + debounced.cancel = cancel; + return debounced; } /** @@ -5732,11 +5762,12 @@ } /** - * Creates a function that, when executed, will only call the `func` function - * at most once per every `wait` milliseconds. 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 throttled function will - * return the result of the last `func` call. + * Creates a function that will only call the `func` function at most once + * per every `wait` milliseconds. The created function comes with a `cancel` + * method to cancel delayed calls. 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 throttled function will return the result + * of the last `func` call. * * 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 @@ -5754,13 +5785,14 @@ * @example * * // avoid excessively updating the position while scrolling - * var throttled = _.throttle(updatePosition, 100); - * jQuery(window).on('scroll', throttled); + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes - * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { - * 'trailing': false - * })); + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }) + * jQuery('.interactive').on('click',); + * + * // cancelling a trailing throttled call + * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, diff --git a/test/test.js b/test/test.js index b329635e4..d6bc0e3a3 100644 --- a/test/test.js +++ b/test/test.js @@ -9412,6 +9412,28 @@ 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(); + } + }); }); /*--------------------------------------------------------------------------*/