Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 0bf6110

Browse files
committed
feat(scope): throw exception when recursive $apply
1 parent acb4338 commit 0bf6110

File tree

2 files changed

+87
-22
lines changed

2 files changed

+87
-22
lines changed

src/service/scope.js

+21-11
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
*/
3737
function $RootScopeProvider(){
3838
this.$get = ['$injector', '$exceptionHandler', '$parse',
39-
function( $injector, $exceptionHandler, $parse){
39+
function( $injector, $exceptionHandler, $parse) {
40+
4041
/**
4142
* @ngdoc function
4243
* @name angular.module.ng.$rootScope.Scope
@@ -152,8 +153,7 @@ function $RootScopeProvider(){
152153
child.$parent = this;
153154
child.$id = nextUid();
154155
child.$$asyncQueue = [];
155-
child.$$phase = child.$$watchers =
156-
child.$$nextSibling = child.$$childHead = child.$$childTail = null;
156+
child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
157157
child.$$prevSibling = this.$$childTail;
158158
if (this.$$childHead) {
159159
this.$$childTail.$$nextSibling = child;
@@ -326,15 +326,12 @@ function $RootScopeProvider(){
326326
watchLog = [],
327327
logIdx, logMsg;
328328

329-
if (target.$$phase) {
330-
throw Error(target.$$phase + ' already in progress');
331-
}
332-
do {
329+
flagPhase(target, '$digest');
333330

331+
do {
334332
dirty = false;
335333
current = target;
336334
do {
337-
current.$$phase = '$digest';
338335
asyncQueue = current.$$asyncQueue;
339336
while(asyncQueue.length) {
340337
try {
@@ -356,7 +353,7 @@ function $RootScopeProvider(){
356353
watch.last = copy(value);
357354
watch.fn(current, value, ((last === initWatchVal) ? value : last));
358355
if (ttl < 5) {
359-
logIdx = 4-ttl;
356+
logIdx = 4 - ttl;
360357
if (!watchLog[logIdx]) watchLog[logIdx] = [];
361358
logMsg = (isFunction(watch.exp))
362359
? 'fn: ' + (watch.exp.name || watch.exp.toString())
@@ -371,8 +368,6 @@ function $RootScopeProvider(){
371368
}
372369
}
373370

374-
current.$$phase = null;
375-
376371
// Insanity Warning: scope depth-first traversal
377372
// yes, this code is a bit crazy, but it works and we have tests to prove it!
378373
// this piece should be kept in sync with the traversal in $broadcast
@@ -388,6 +383,8 @@ function $RootScopeProvider(){
388383
'Watchers fired in the last 5 iterations: ' + toJson(watchLog));
389384
}
390385
} while (dirty || asyncQueue.length);
386+
387+
this.$root.$$phase = null;
391388
},
392389

393390
/**
@@ -524,10 +521,12 @@ function $RootScopeProvider(){
524521
*/
525522
$apply: function(expr) {
526523
try {
524+
flagPhase(this, '$apply');
527525
return this.$eval(expr);
528526
} catch (e) {
529527
$exceptionHandler(e);
530528
} finally {
529+
this.$root.$$phase = null;
531530
this.$root.$digest();
532531
}
533532
},
@@ -671,6 +670,17 @@ function $RootScopeProvider(){
671670
}
672671
};
673672

673+
674+
function flagPhase(scope, phase) {
675+
var root = scope.$root;
676+
677+
if (root.$$phase) {
678+
throw Error(root.$$phase + ' already in progress');
679+
}
680+
681+
root.$$phase = phase;
682+
}
683+
674684
return new Scope();
675685

676686
function compileToFn(exp, name) {

test/service/scopeSpec.js

+66-11
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
describe('Scope', function() {
44

5-
beforeEach(inject(function($exceptionHandlerProvider) {
6-
$exceptionHandlerProvider.mode('log');
7-
}));
8-
9-
105
describe('$root', function() {
116
it('should point to itself', inject(function($rootScope) {
127
expect($rootScope.$root).toEqual($rootScope);
@@ -122,7 +117,9 @@ describe('Scope', function() {
122117
}));
123118

124119

125-
it('should delegate exceptions', inject(function($rootScope, $exceptionHandler, $log) {
120+
it('should delegate exceptions', inject(function($exceptionHandlerProvider) {
121+
$exceptionHandlerProvider.mode('log');
122+
}, function($rootScope, $exceptionHandler, $log) {
126123
$rootScope.$watch('a', function() {throw new Error('abc');});
127124
$rootScope.a = 1;
128125
$rootScope.$digest();
@@ -227,7 +224,7 @@ describe('Scope', function() {
227224
}));
228225

229226

230-
it('should prevent infinite recurcion and print print watcher function name or body',
227+
it('should prevent infinite recursion and print print watcher function name or body',
231228
inject(function($rootScope) {
232229
$rootScope.$watch(function watcherA() {return $rootScope.a;}, function(self) {self.b++;});
233230
$rootScope.$watch(function() {return $rootScope.b;}, function(self) {self.a++;});
@@ -277,7 +274,7 @@ describe('Scope', function() {
277274
}));
278275

279276

280-
it('should prevent recursion', inject(function($rootScope) {
277+
it('should prevent $digest recursion', inject(function($rootScope) {
281278
var callCount = 0;
282279
$rootScope.$watch('name', function() {
283280
expect(function() {
@@ -462,7 +459,9 @@ describe('Scope', function() {
462459
}));
463460

464461

465-
it('should catch exceptions', inject(function($rootScope, $exceptionHandler, $log) {
462+
it('should catch exceptions', inject(function($exceptionHandlerProvider) {
463+
$exceptionHandlerProvider.mode('log');
464+
}, function($rootScope, $exceptionHandler, $log) {
466465
var log = '';
467466
var child = $rootScope.$new();
468467
$rootScope.$watch('a', function(scope, a) { log += '1'; });
@@ -476,7 +475,9 @@ describe('Scope', function() {
476475

477476
describe('exceptions', function() {
478477
var log;
479-
beforeEach(inject(function($rootScope) {
478+
beforeEach(inject(function($exceptionHandlerProvider) {
479+
$exceptionHandlerProvider.mode('log');
480+
}, function($rootScope) {
480481
log = '';
481482
$rootScope.$watch(function() { log += '$digest;'; });
482483
$rootScope.$digest();
@@ -502,6 +503,57 @@ describe('Scope', function() {
502503
expect($exceptionHandler.errors).toEqual([error]);
503504
}));
504505
});
506+
507+
508+
describe('recursive $apply protection', function() {
509+
it('should throw an exception if $apply is called while an $apply is in progress', inject(
510+
function($rootScope) {
511+
expect(function() {
512+
$rootScope.$apply(function() {
513+
$rootScope.$apply();
514+
});
515+
}).toThrow('$apply already in progress');
516+
}));
517+
518+
519+
it('should throw an exception if $apply is called while flushing evalAsync queue', inject(
520+
function($rootScope) {
521+
expect(function() {
522+
$rootScope.$apply(function() {
523+
$rootScope.$evalAsync(function() {
524+
$rootScope.$apply();
525+
});
526+
});
527+
}).toThrow('$digest already in progress');
528+
}));
529+
530+
531+
it('should throw an exception if $apply is called while a watch is being initialized', inject(
532+
function($rootScope) {
533+
var childScope1 = $rootScope.$new();
534+
childScope1.$watch('x', function() {
535+
childScope1.$apply();
536+
});
537+
expect(function() { childScope1.$apply(); }).toThrow('$digest already in progress');
538+
}));
539+
540+
541+
it('should thrown an exception if $apply in called from a watch fn (after init)', inject(
542+
function($rootScope) {
543+
var childScope2 = $rootScope.$new();
544+
childScope2.$apply(function() {
545+
childScope2.$watch('x', function(scope, newVal, oldVal) {
546+
if (newVal !== oldVal) {
547+
childScope2.$apply();
548+
}
549+
});
550+
});
551+
552+
expect(function() { childScope2.$apply(function() {
553+
childScope2.x = 'something';
554+
}); }).toThrow('$digest already in progress');
555+
}));
556+
});
505557
});
506558

507559

@@ -561,7 +613,10 @@ describe('Scope', function() {
561613
log += event.currentScope.id + '>';
562614
}
563615

564-
beforeEach(inject(function($rootScope) {
616+
beforeEach(inject(
617+
function($exceptionHandlerProvider) {
618+
$exceptionHandlerProvider.mode('log');
619+
}, function($rootScope) {
565620
log = '';
566621
child = $rootScope.$new();
567622
grandChild = child.$new();

0 commit comments

Comments
 (0)