fix($timeout/$interval): do not trigger a digest on cancel

Previously, `.catch(noop)` was used on a rejected timeout/interval to prevent an unhandled rejection error (introduced in #c9dffde1cb). However this would schedule a deferred task to run the `noop`. If the cancelling was outside a digest this could cause a new digest such as with the ng-model `debounce` option.

For unit testing, this means that it's no longer necessary to use `$timeout.flush()` when a `$timeout` has been cancelled outside of a digest. Previously, this was necessary to execute the deferred task added by `.catch(noop).
There's an example of such a change in this commit's changeset in the file `/test/ngAnimate/animateCssSpec.js`.

Fixes #16057
Closes #16064
This commit is contained in:
Jason Bedard
2017-07-03 02:04:17 -07:00
committed by Martin Staffa
parent bf0af6dbb1
commit cdaa6a951b
8 changed files with 48 additions and 8 deletions
+3
View File
@@ -168,6 +168,9 @@
/* ng/compile.js */
"directiveNormalize": false,
/* ng/q.js */
"markQExceptionHandled": false,
/* ng/directive/directives.js */
"ngDirective": false,
+1 -1
View File
@@ -190,7 +190,7 @@ function $IntervalProvider() {
interval.cancel = function(promise) {
if (promise && promise.$$intervalId in intervals) {
// Interval cancels should not report as unhandled promise.
intervals[promise.$$intervalId].promise.catch(noop);
markQExceptionHandled(intervals[promise.$$intervalId].promise);
intervals[promise.$$intervalId].reject('canceled');
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
+14 -4
View File
@@ -351,7 +351,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
state.pending = undefined;
try {
for (var i = 0, ii = pending.length; i < ii; ++i) {
state.pur = true;
markQStateExceptionHandled(state);
promise = pending[i][0];
fn = pending[i][state.status];
try {
@@ -378,8 +378,8 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
// eslint-disable-next-line no-unmodified-loop-condition
while (!queueSize && checkQueue.length) {
var toCheck = checkQueue.shift();
if (!toCheck.pur) {
toCheck.pur = true;
if (!isStateExceptionHandled(toCheck)) {
markQStateExceptionHandled(toCheck);
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
if (isError(toCheck.value)) {
exceptionHandler(toCheck.value, errorMessage);
@@ -391,7 +391,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
}
function scheduleProcessQueue(state) {
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) {
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) {
if (queueSize === 0 && checkQueue.length === 0) {
nextTick(processChecks);
}
@@ -671,3 +671,13 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
return $Q;
}
function isStateExceptionHandled(state) {
return !!state.pur;
}
function markQStateExceptionHandled(state) {
state.pur = true;
}
function markQExceptionHandled(q) {
markQStateExceptionHandled(q.$$state);
}
+1 -1
View File
@@ -85,7 +85,7 @@ function $TimeoutProvider() {
timeout.cancel = function(promise) {
if (promise && promise.$$timeoutId in deferreds) {
// Timeout cancels should not report an unhandled promise.
deferreds[promise.$$timeoutId].promise.catch(noop);
markQExceptionHandled(deferreds[promise.$$timeoutId].promise);
deferreds[promise.$$timeoutId].reject('canceled');
delete deferreds[promise.$$timeoutId];
return $browser.defer.cancel(promise.$$timeoutId);
+6
View File
@@ -459,8 +459,14 @@ describe('ngModelOptions', function() {
$rootScope.$watch(watchSpy);
helper.changeInputValueTo('a');
$timeout.flush(2000);
expect(watchSpy).not.toHaveBeenCalled();
helper.changeInputValueTo('b');
$timeout.flush(2000);
expect(watchSpy).not.toHaveBeenCalled();
helper.changeInputValueTo('c');
$timeout.flush(10000);
expect(watchSpy).toHaveBeenCalled();
});
+11
View File
@@ -339,6 +339,17 @@ describe('$interval', function() {
inject(function($interval) {
expect($interval.cancel()).toBe(false);
}));
it('should not trigger digest when cancelled', inject(function($interval, $rootScope, $browser) {
var watchSpy = jasmine.createSpy('watchSpy');
$rootScope.$watch(watchSpy);
var t = $interval();
$interval.cancel(t);
expect(function() {$browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
expect(watchSpy).not.toHaveBeenCalled();
}));
});
describe('$window delegation', function() {
+11
View File
@@ -299,5 +299,16 @@ describe('$timeout', function() {
$timeout.cancel(promise);
expect(cancelSpy).toHaveBeenCalledOnce();
}));
it('should not trigger digest when cancelled', inject(function($timeout, $rootScope, $browser) {
var watchSpy = jasmine.createSpy('watchSpy');
$rootScope.$watch(watchSpy);
var t = $timeout();
$timeout.cancel(t);
expect(function() {$browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
expect(watchSpy).not.toHaveBeenCalled();
}));
});
});
+1 -2
View File
@@ -1328,8 +1328,7 @@ describe('ngAnimate $animateCss', function() {
animator.end();
expect(element.data(ANIMATE_TIMER_KEY)).toBeUndefined();
$timeout.flush();
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
$timeout.verifyNoPendingTasks();
}));
});