From 6902e2f36a208752d08cb81c3b7346638ee48170 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 22 Aug 2013 02:08:41 -0700 Subject: [PATCH] feat(Scope): async auto-flush $evalAsync queue when outside of $digest This change causes a new $digest to be scheduled in the next tick if a task was was sent to the $evalAsync queue from outside of a $digest or an $apply. While this mode of operation is not common for most of the user code, this change means that $q promises that utilze $evalAsync queue to guarantee asynchronicity of promise apis will now also resolve outside of a $digest, which turned out to be a big pain point for some developers. The implementation ensures that we don't do more work than needed and that we coalese as much work as possible into a single $digest. The use of $browser instead of setTimeout ensures that we can mock out and control the scheduling of "auto-flush", which should in theory allow all of the existing code and tests to work without negative side-effects. Closes #3539 Closes #2438 --- src/ng/rootScope.js | 23 ++++++++++++++---- src/ng/timeout.js | 2 +- test/ng/rootScopeSpec.js | 51 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index d94a621d94c6..18c544340c44 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -69,8 +69,8 @@ function $RootScopeProvider(){ return TTL; }; - this.$get = ['$injector', '$exceptionHandler', '$parse', - function( $injector, $exceptionHandler, $parse) { + this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', + function( $injector, $exceptionHandler, $parse, $browser) { /** * @ngdoc function @@ -666,13 +666,16 @@ function $RootScopeProvider(){ * * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: * - * - it will execute in the current script execution context (before any DOM rendering). - * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after - * `expression` execution. + * - it will execute after the function that schedule the evaluation is done running (preferably before DOM rendering). + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after `expression` execution. * * Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. * + * __Note:__ if this function is called outside of `$digest` cycle, a new $digest cycle will be scheduled. + * It is however encouraged to always call code that changes the model from withing an `$apply` call. + * That includes code evaluated via `$evalAsync`. + * * @param {(string|function())=} expression An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. @@ -680,6 +683,16 @@ function $RootScopeProvider(){ * */ $evalAsync: function(expr) { + // if we are outside of an $digest loop and this is the first time we are scheduling async task also schedule + // async auto-flush + if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + $browser.defer(function() { + if ($rootScope.$$asyncQueue.length) { + $rootScope.$digest(); + } + }); + } + this.$$asyncQueue.push(expr); }, diff --git a/src/ng/timeout.js b/src/ng/timeout.js index 6cb62d7a449b..a32538ee9b0b 100644 --- a/src/ng/timeout.js +++ b/src/ng/timeout.js @@ -36,7 +36,7 @@ function $TimeoutProvider() { var deferred = $q.defer(), promise = deferred.promise, skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId, cleanup; + timeoutId; timeoutId = $browser.defer(function() { try { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index ddd830881d9b..0a85d9a85085 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -705,6 +705,57 @@ describe('Scope', function() { expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue); expect($rootScope.$$asyncQueue).toEqual(['rootExpression', 'childExpression', 'isolateExpression']); })); + + + describe('auto-flushing when queueing outside of an $apply', function() { + var log, $rootScope, $browser; + + beforeEach(inject(function(_log_, _$rootScope_, _$browser_) { + log = _log_; + $rootScope = _$rootScope_; + $browser = _$browser_; + })); + + + it('should auto-flush the queue asynchronously and trigger digest', function() { + $rootScope.$evalAsync(log.fn('eval-ed!')); + $rootScope.$watch(log.fn('digesting')); + expect(log).toEqual([]); + + $browser.defer.flush(0); + + expect(log).toEqual(['eval-ed!', 'digesting', 'digesting']); + }); + + + it('should not trigger digest asynchronously if the queue is empty in the next tick', function() { + $rootScope.$evalAsync(log.fn('eval-ed!')); + $rootScope.$watch(log.fn('digesting')); + expect(log).toEqual([]); + + $rootScope.$digest(); + + expect(log).toEqual(['eval-ed!', 'digesting', 'digesting']); + log.reset(); + + $browser.defer.flush(0); + + expect(log).toEqual([]); + }); + + + it('should not schedule more than one auto-flush task', function() { + $rootScope.$evalAsync(log.fn('eval-ed 1!')); + $rootScope.$evalAsync(log.fn('eval-ed 2!')); + + $browser.defer.flush(0); + expect(log).toEqual(['eval-ed 1!', 'eval-ed 2!']); + + expect(function() { + $browser.defer.flush(0); + }).toThrow('No deferred tasks with delay up to 0ms to be flushed!'); + }); + }); });