mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-25 22:33:44 +00:00
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
This commit is contained in:
parent
42af8eada2
commit
6b91aa0a18
3 changed files with 70 additions and 6 deletions
|
|
@ -69,8 +69,8 @@ function $RootScopeProvider(){
|
||||||
return TTL;
|
return TTL;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$get = ['$injector', '$exceptionHandler', '$parse',
|
this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
|
||||||
function( $injector, $exceptionHandler, $parse) {
|
function( $injector, $exceptionHandler, $parse, $browser) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
|
|
@ -666,13 +666,16 @@ function $RootScopeProvider(){
|
||||||
*
|
*
|
||||||
* The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that:
|
* 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).
|
* - 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
|
* - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after `expression` execution.
|
||||||
* `expression` execution.
|
|
||||||
*
|
*
|
||||||
* Any exceptions from the execution of the expression are forwarded to the
|
* Any exceptions from the execution of the expression are forwarded to the
|
||||||
* {@link ng.$exceptionHandler $exceptionHandler} service.
|
* {@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.
|
* @param {(string|function())=} expression An angular expression to be executed.
|
||||||
*
|
*
|
||||||
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
|
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
|
||||||
|
|
@ -680,6 +683,16 @@ function $RootScopeProvider(){
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
$evalAsync: function(expr) {
|
$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);
|
this.$$asyncQueue.push(expr);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ function $TimeoutProvider() {
|
||||||
var deferred = $q.defer(),
|
var deferred = $q.defer(),
|
||||||
promise = deferred.promise,
|
promise = deferred.promise,
|
||||||
skipApply = (isDefined(invokeApply) && !invokeApply),
|
skipApply = (isDefined(invokeApply) && !invokeApply),
|
||||||
timeoutId, cleanup;
|
timeoutId;
|
||||||
|
|
||||||
timeoutId = $browser.defer(function() {
|
timeoutId = $browser.defer(function() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,57 @@ describe('Scope', function() {
|
||||||
expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
|
expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
|
||||||
expect($rootScope.$$asyncQueue).toEqual(['rootExpression', 'childExpression', 'isolateExpression']);
|
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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue