mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-19 20:01:52 +00:00
fix($timeout): clean deferreds immediately after callback exec/cancel
Make sure $timeout callbacks are forgotten about immediately after execution or cancellation. Previously when passing invokeApply=false, the cleanup used $q and so would be pending until the next $digest was triggered. This does not make a large functional difference, but can be very visible when looking at memory consumption of an app or debugging around the $$asyncQueue - these callbacks can have a big retaining tree.
This commit is contained in:
parent
f757f86b6c
commit
920a380413
2 changed files with 55 additions and 5 deletions
|
|
@ -45,17 +45,15 @@ function $TimeoutProvider() {
|
||||||
deferred.reject(e);
|
deferred.reject(e);
|
||||||
$exceptionHandler(e);
|
$exceptionHandler(e);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
delete deferreds[promise.$$timeoutId];
|
||||||
|
}
|
||||||
|
|
||||||
if (!skipApply) $rootScope.$apply();
|
if (!skipApply) $rootScope.$apply();
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
||||||
cleanup = function() {
|
|
||||||
delete deferreds[promise.$$timeoutId];
|
|
||||||
};
|
|
||||||
|
|
||||||
promise.$$timeoutId = timeoutId;
|
promise.$$timeoutId = timeoutId;
|
||||||
deferreds[timeoutId] = deferred;
|
deferreds[timeoutId] = deferred;
|
||||||
promise.then(cleanup, cleanup);
|
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +75,7 @@ function $TimeoutProvider() {
|
||||||
timeout.cancel = function(promise) {
|
timeout.cancel = function(promise) {
|
||||||
if (promise && promise.$$timeoutId in deferreds) {
|
if (promise && promise.$$timeoutId in deferreds) {
|
||||||
deferreds[promise.$$timeoutId].reject('canceled');
|
deferreds[promise.$$timeoutId].reject('canceled');
|
||||||
|
delete deferreds[promise.$$timeoutId];
|
||||||
return $browser.defer.cancel(promise.$$timeoutId);
|
return $browser.defer.cancel(promise.$$timeoutId);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,27 @@ describe('$timeout', function() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should forget references to deferreds when callback called even if skipApply is true',
|
||||||
|
inject(function($timeout, $browser) {
|
||||||
|
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
|
||||||
|
var cancelSpy = spyOn($browser.defer, 'cancel').andCallThrough();
|
||||||
|
|
||||||
|
var promise1 = $timeout(function() {}, 0, false);
|
||||||
|
var promise2 = $timeout(function() {}, 100, false);
|
||||||
|
expect(cancelSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
$timeout.flushNext(0);
|
||||||
|
|
||||||
|
// Promise1 deferred object should already be removed from the list and not cancellable
|
||||||
|
$timeout.cancel(promise1);
|
||||||
|
expect(cancelSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Promise2 deferred object should not have been called and should be cancellable
|
||||||
|
$timeout.cancel(promise2);
|
||||||
|
expect(cancelSpy).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
describe('exception handling', function() {
|
describe('exception handling', function() {
|
||||||
|
|
||||||
beforeEach(module(function($exceptionHandlerProvider) {
|
beforeEach(module(function($exceptionHandlerProvider) {
|
||||||
|
|
@ -106,6 +127,20 @@ describe('$timeout', function() {
|
||||||
|
|
||||||
expect(log).toEqual('error: Some Error');
|
expect(log).toEqual('error: Some Error');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should forget references to relevant deferred even when exception is thrown',
|
||||||
|
inject(function($timeout, $browser) {
|
||||||
|
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
|
||||||
|
var cancelSpy = spyOn($browser.defer, 'cancel').andCallThrough();
|
||||||
|
|
||||||
|
var promise = $timeout(function() { throw "Test Error"; }, 0, false);
|
||||||
|
$timeout.flush();
|
||||||
|
|
||||||
|
expect(cancelSpy).not.toHaveBeenCalled();
|
||||||
|
$timeout.cancel(promise);
|
||||||
|
expect(cancelSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,5 +182,21 @@ describe('$timeout', function() {
|
||||||
it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) {
|
it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) {
|
||||||
expect($timeout.cancel()).toBe(false);
|
expect($timeout.cancel()).toBe(false);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should forget references to relevant deferred', inject(function($timeout, $browser) {
|
||||||
|
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
|
||||||
|
var cancelSpy = spyOn($browser.defer, 'cancel').andCallThrough();
|
||||||
|
|
||||||
|
var promise = $timeout(function() {}, 0, false);
|
||||||
|
|
||||||
|
expect(cancelSpy).not.toHaveBeenCalled();
|
||||||
|
$timeout.cancel(promise);
|
||||||
|
expect(cancelSpy).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
// Promise deferred object should already be removed from the list and not cancellable again
|
||||||
|
$timeout.cancel(promise);
|
||||||
|
expect(cancelSpy).toHaveBeenCalledOnce();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue