diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 198d48b6242d..eddcf61da4d6 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -769,15 +769,19 @@ function $RootScopeProvider() { dirty = false; current = target; - while (asyncQueue.length) { + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $applyAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { - asyncTask = asyncQueue.shift(); + asyncTask = asyncQueue[asyncQueuePosition]; asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } + asyncQueue.length = 0; traverseScopesLoop: do { // "traverse the scopes" loop @@ -848,13 +852,15 @@ function $RootScopeProvider() { clearPhase(); - while (postDigestQueue.length) { + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { try { - postDigestQueue.shift()(); + postDigestQueue[postDigestQueuePosition++](); } catch (e) { $exceptionHandler(e); } } + postDigestQueue.length = postDigestQueuePosition = 0; }, @@ -1309,6 +1315,8 @@ function $RootScopeProvider() { var postDigestQueue = $rootScope.$$postDigestQueue = []; var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + var postDigestQueuePosition = 0; + return $rootScope; diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index f15fa701ef2b..70e565b7b434 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1320,46 +1320,6 @@ describe('Scope', function() { expect(externalWatchCount).toEqual(0); })); - it('should run a $$postDigest call on all child scopes when a parent scope is digested', inject(function($rootScope) { - var parent = $rootScope.$new(), - child = parent.$new(), - count = 0; - - $rootScope.$$postDigest(function() { - count++; - }); - - parent.$$postDigest(function() { - count++; - }); - - child.$$postDigest(function() { - count++; - }); - - expect(count).toBe(0); - $rootScope.$digest(); - expect(count).toBe(3); - })); - - it('should run a $$postDigest call even if the child scope is isolated', inject(function($rootScope) { - var parent = $rootScope.$new(), - child = parent.$new(true), - signature = ''; - - parent.$$postDigest(function() { - signature += 'A'; - }); - - child.$$postDigest(function() { - signature += 'B'; - }); - - expect(signature).toBe(''); - $rootScope.$digest(); - expect(signature).toBe('AB'); - })); - it('should cause a $digest rerun', inject(function($rootScope) { $rootScope.log = ''; $rootScope.value = 0; @@ -1705,6 +1665,92 @@ describe('Scope', function() { })); }); + describe('$$postDigest', function() { + it('should process callbacks as a queue (FIFO) when the scope is digested', inject(function($rootScope) { + var signature = ''; + + $rootScope.$$postDigest(function() { + signature += 'A'; + $rootScope.$$postDigest(function() { + signature += 'D'; + }); + }); + + $rootScope.$$postDigest(function() { + signature += 'B'; + }); + + $rootScope.$$postDigest(function() { + signature += 'C'; + }); + + expect(signature).toBe(''); + $rootScope.$digest(); + expect(signature).toBe('ABCD'); + })); + + it('should support $apply calls nested in $$postDigest callbacks', inject(function($rootScope) { + var signature = ''; + + $rootScope.$$postDigest(function() { + signature += 'A'; + }); + + $rootScope.$$postDigest(function() { + signature += 'B'; + $rootScope.$apply(); + signature += 'D'; + }); + + $rootScope.$$postDigest(function() { + signature += 'C'; + }); + + expect(signature).toBe(''); + $rootScope.$digest(); + expect(signature).toBe('ABCD'); + })); + + it('should run a $$postDigest call on all child scopes when a parent scope is digested', inject(function($rootScope) { + var parent = $rootScope.$new(), + child = parent.$new(), + count = 0; + + $rootScope.$$postDigest(function() { + count++; + }); + + parent.$$postDigest(function() { + count++; + }); + + child.$$postDigest(function() { + count++; + }); + + expect(count).toBe(0); + $rootScope.$digest(); + expect(count).toBe(3); + })); + + it('should run a $$postDigest call even if the child scope is isolated', inject(function($rootScope) { + var parent = $rootScope.$new(), + child = parent.$new(true), + signature = ''; + + parent.$$postDigest(function() { + signature += 'A'; + }); + + child.$$postDigest(function() { + signature += 'B'; + }); + + expect(signature).toBe(''); + $rootScope.$digest(); + expect(signature).toBe('AB'); + })); + }); describe('events', function() {