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

Commit 6b91aa0

Browse files
committed
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
1 parent 42af8ea commit 6b91aa0

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

src/ng/rootScope.js

+18-5
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ function $RootScopeProvider(){
6969
return TTL;
7070
};
7171

72-
this.$get = ['$injector', '$exceptionHandler', '$parse',
73-
function( $injector, $exceptionHandler, $parse) {
72+
this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
73+
function( $injector, $exceptionHandler, $parse, $browser) {
7474

7575
/**
7676
* @ngdoc function
@@ -666,20 +666,33 @@ function $RootScopeProvider(){
666666
*
667667
* The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that:
668668
*
669-
* - it will execute in the current script execution context (before any DOM rendering).
670-
* - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after
671-
* `expression` execution.
669+
* - it will execute after the function that schedule the evaluation is done running (preferably before DOM rendering).
670+
* - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after `expression` execution.
672671
*
673672
* Any exceptions from the execution of the expression are forwarded to the
674673
* {@link ng.$exceptionHandler $exceptionHandler} service.
675674
*
675+
* __Note:__ if this function is called outside of `$digest` cycle, a new $digest cycle will be scheduled.
676+
* It is however encouraged to always call code that changes the model from withing an `$apply` call.
677+
* That includes code evaluated via `$evalAsync`.
678+
*
676679
* @param {(string|function())=} expression An angular expression to be executed.
677680
*
678681
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
679682
* - `function(scope)`: execute the function with the current `scope` parameter.
680683
*
681684
*/
682685
$evalAsync: function(expr) {
686+
// if we are outside of an $digest loop and this is the first time we are scheduling async task also schedule
687+
// async auto-flush
688+
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
689+
$browser.defer(function() {
690+
if ($rootScope.$$asyncQueue.length) {
691+
$rootScope.$digest();
692+
}
693+
});
694+
}
695+
683696
this.$$asyncQueue.push(expr);
684697
},
685698

src/ng/timeout.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function $TimeoutProvider() {
3636
var deferred = $q.defer(),
3737
promise = deferred.promise,
3838
skipApply = (isDefined(invokeApply) && !invokeApply),
39-
timeoutId, cleanup;
39+
timeoutId;
4040

4141
timeoutId = $browser.defer(function() {
4242
try {

test/ng/rootScopeSpec.js

+51
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,57 @@ describe('Scope', function() {
705705
expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
706706
expect($rootScope.$$asyncQueue).toEqual(['rootExpression', 'childExpression', 'isolateExpression']);
707707
}));
708+
709+
710+
describe('auto-flushing when queueing outside of an $apply', function() {
711+
var log, $rootScope, $browser;
712+
713+
beforeEach(inject(function(_log_, _$rootScope_, _$browser_) {
714+
log = _log_;
715+
$rootScope = _$rootScope_;
716+
$browser = _$browser_;
717+
}));
718+
719+
720+
it('should auto-flush the queue asynchronously and trigger digest', function() {
721+
$rootScope.$evalAsync(log.fn('eval-ed!'));
722+
$rootScope.$watch(log.fn('digesting'));
723+
expect(log).toEqual([]);
724+
725+
$browser.defer.flush(0);
726+
727+
expect(log).toEqual(['eval-ed!', 'digesting', 'digesting']);
728+
});
729+
730+
731+
it('should not trigger digest asynchronously if the queue is empty in the next tick', function() {
732+
$rootScope.$evalAsync(log.fn('eval-ed!'));
733+
$rootScope.$watch(log.fn('digesting'));
734+
expect(log).toEqual([]);
735+
736+
$rootScope.$digest();
737+
738+
expect(log).toEqual(['eval-ed!', 'digesting', 'digesting']);
739+
log.reset();
740+
741+
$browser.defer.flush(0);
742+
743+
expect(log).toEqual([]);
744+
});
745+
746+
747+
it('should not schedule more than one auto-flush task', function() {
748+
$rootScope.$evalAsync(log.fn('eval-ed 1!'));
749+
$rootScope.$evalAsync(log.fn('eval-ed 2!'));
750+
751+
$browser.defer.flush(0);
752+
expect(log).toEqual(['eval-ed 1!', 'eval-ed 2!']);
753+
754+
expect(function() {
755+
$browser.defer.flush(0);
756+
}).toThrow('No deferred tasks with delay up to 0ms to be flushed!');
757+
});
758+
});
708759
});
709760

710761

0 commit comments

Comments
 (0)