feat(scope): throw exception when recursive $apply

This commit is contained in:
Igor Minar 2012-01-04 12:02:39 -08:00
parent acb4338b70
commit 0bf611087b
2 changed files with 87 additions and 22 deletions

View file

@ -36,7 +36,8 @@
*/ */
function $RootScopeProvider(){ function $RootScopeProvider(){
this.$get = ['$injector', '$exceptionHandler', '$parse', this.$get = ['$injector', '$exceptionHandler', '$parse',
function( $injector, $exceptionHandler, $parse){ function( $injector, $exceptionHandler, $parse) {
/** /**
* @ngdoc function * @ngdoc function
* @name angular.module.ng.$rootScope.Scope * @name angular.module.ng.$rootScope.Scope
@ -152,8 +153,7 @@ function $RootScopeProvider(){
child.$parent = this; child.$parent = this;
child.$id = nextUid(); child.$id = nextUid();
child.$$asyncQueue = []; child.$$asyncQueue = [];
child.$$phase = child.$$watchers = child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
child.$$nextSibling = child.$$childHead = child.$$childTail = null;
child.$$prevSibling = this.$$childTail; child.$$prevSibling = this.$$childTail;
if (this.$$childHead) { if (this.$$childHead) {
this.$$childTail.$$nextSibling = child; this.$$childTail.$$nextSibling = child;
@ -326,15 +326,12 @@ function $RootScopeProvider(){
watchLog = [], watchLog = [],
logIdx, logMsg; logIdx, logMsg;
if (target.$$phase) { flagPhase(target, '$digest');
throw Error(target.$$phase + ' already in progress');
}
do {
do {
dirty = false; dirty = false;
current = target; current = target;
do { do {
current.$$phase = '$digest';
asyncQueue = current.$$asyncQueue; asyncQueue = current.$$asyncQueue;
while(asyncQueue.length) { while(asyncQueue.length) {
try { try {
@ -356,7 +353,7 @@ function $RootScopeProvider(){
watch.last = copy(value); watch.last = copy(value);
watch.fn(current, value, ((last === initWatchVal) ? value : last)); watch.fn(current, value, ((last === initWatchVal) ? value : last));
if (ttl < 5) { if (ttl < 5) {
logIdx = 4-ttl; logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = []; if (!watchLog[logIdx]) watchLog[logIdx] = [];
logMsg = (isFunction(watch.exp)) logMsg = (isFunction(watch.exp))
? 'fn: ' + (watch.exp.name || watch.exp.toString()) ? 'fn: ' + (watch.exp.name || watch.exp.toString())
@ -371,8 +368,6 @@ function $RootScopeProvider(){
} }
} }
current.$$phase = null;
// Insanity Warning: scope depth-first traversal // Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it! // yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast // this piece should be kept in sync with the traversal in $broadcast
@ -388,6 +383,8 @@ function $RootScopeProvider(){
'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); 'Watchers fired in the last 5 iterations: ' + toJson(watchLog));
} }
} while (dirty || asyncQueue.length); } while (dirty || asyncQueue.length);
this.$root.$$phase = null;
}, },
/** /**
@ -524,10 +521,12 @@ function $RootScopeProvider(){
*/ */
$apply: function(expr) { $apply: function(expr) {
try { try {
flagPhase(this, '$apply');
return this.$eval(expr); return this.$eval(expr);
} catch (e) { } catch (e) {
$exceptionHandler(e); $exceptionHandler(e);
} finally { } finally {
this.$root.$$phase = null;
this.$root.$digest(); this.$root.$digest();
} }
}, },
@ -671,6 +670,17 @@ function $RootScopeProvider(){
} }
}; };
function flagPhase(scope, phase) {
var root = scope.$root;
if (root.$$phase) {
throw Error(root.$$phase + ' already in progress');
}
root.$$phase = phase;
}
return new Scope(); return new Scope();
function compileToFn(exp, name) { function compileToFn(exp, name) {

View file

@ -2,11 +2,6 @@
describe('Scope', function() { describe('Scope', function() {
beforeEach(inject(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));
describe('$root', function() { describe('$root', function() {
it('should point to itself', inject(function($rootScope) { it('should point to itself', inject(function($rootScope) {
expect($rootScope.$root).toEqual($rootScope); expect($rootScope.$root).toEqual($rootScope);
@ -122,7 +117,9 @@ describe('Scope', function() {
})); }));
it('should delegate exceptions', inject(function($rootScope, $exceptionHandler, $log) { it('should delegate exceptions', inject(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}, function($rootScope, $exceptionHandler, $log) {
$rootScope.$watch('a', function() {throw new Error('abc');}); $rootScope.$watch('a', function() {throw new Error('abc');});
$rootScope.a = 1; $rootScope.a = 1;
$rootScope.$digest(); $rootScope.$digest();
@ -227,7 +224,7 @@ describe('Scope', function() {
})); }));
it('should prevent infinite recurcion and print print watcher function name or body', it('should prevent infinite recursion and print print watcher function name or body',
inject(function($rootScope) { inject(function($rootScope) {
$rootScope.$watch(function watcherA() {return $rootScope.a;}, function(self) {self.b++;}); $rootScope.$watch(function watcherA() {return $rootScope.a;}, function(self) {self.b++;});
$rootScope.$watch(function() {return $rootScope.b;}, function(self) {self.a++;}); $rootScope.$watch(function() {return $rootScope.b;}, function(self) {self.a++;});
@ -277,7 +274,7 @@ describe('Scope', function() {
})); }));
it('should prevent recursion', inject(function($rootScope) { it('should prevent $digest recursion', inject(function($rootScope) {
var callCount = 0; var callCount = 0;
$rootScope.$watch('name', function() { $rootScope.$watch('name', function() {
expect(function() { expect(function() {
@ -462,7 +459,9 @@ describe('Scope', function() {
})); }));
it('should catch exceptions', inject(function($rootScope, $exceptionHandler, $log) { it('should catch exceptions', inject(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}, function($rootScope, $exceptionHandler, $log) {
var log = ''; var log = '';
var child = $rootScope.$new(); var child = $rootScope.$new();
$rootScope.$watch('a', function(scope, a) { log += '1'; }); $rootScope.$watch('a', function(scope, a) { log += '1'; });
@ -476,7 +475,9 @@ describe('Scope', function() {
describe('exceptions', function() { describe('exceptions', function() {
var log; var log;
beforeEach(inject(function($rootScope) { beforeEach(inject(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}, function($rootScope) {
log = ''; log = '';
$rootScope.$watch(function() { log += '$digest;'; }); $rootScope.$watch(function() { log += '$digest;'; });
$rootScope.$digest(); $rootScope.$digest();
@ -502,6 +503,57 @@ describe('Scope', function() {
expect($exceptionHandler.errors).toEqual([error]); expect($exceptionHandler.errors).toEqual([error]);
})); }));
}); });
describe('recursive $apply protection', function() {
it('should throw an exception if $apply is called while an $apply is in progress', inject(
function($rootScope) {
expect(function() {
$rootScope.$apply(function() {
$rootScope.$apply();
});
}).toThrow('$apply already in progress');
}));
it('should throw an exception if $apply is called while flushing evalAsync queue', inject(
function($rootScope) {
expect(function() {
$rootScope.$apply(function() {
$rootScope.$evalAsync(function() {
$rootScope.$apply();
});
});
}).toThrow('$digest already in progress');
}));
it('should throw an exception if $apply is called while a watch is being initialized', inject(
function($rootScope) {
var childScope1 = $rootScope.$new();
childScope1.$watch('x', function() {
childScope1.$apply();
});
expect(function() { childScope1.$apply(); }).toThrow('$digest already in progress');
}));
it('should thrown an exception if $apply in called from a watch fn (after init)', inject(
function($rootScope) {
var childScope2 = $rootScope.$new();
childScope2.$apply(function() {
childScope2.$watch('x', function(scope, newVal, oldVal) {
if (newVal !== oldVal) {
childScope2.$apply();
}
});
});
expect(function() { childScope2.$apply(function() {
childScope2.x = 'something';
}); }).toThrow('$digest already in progress');
}));
});
}); });
@ -561,7 +613,10 @@ describe('Scope', function() {
log += event.currentScope.id + '>'; log += event.currentScope.id + '>';
} }
beforeEach(inject(function($rootScope) { beforeEach(inject(
function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}, function($rootScope) {
log = ''; log = '';
child = $rootScope.$new(); child = $rootScope.$new();
grandChild = child.$new(); grandChild = child.$new();