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

Commit e94d454

Browse files
committed
feat($rootScope): implement $applyAsync to support combining calls to $apply into a single digest.
It is now possible to queue up multiple expressions to be evaluated in a single digest using $applyAsync. The asynchronous expressions will be evaluated either 1) the next time $apply or $rootScope.$digest is called, or 2) after after the queue flushing scheduled for the next turn occurs (roughly ~10ms depending on browser and application).
1 parent 2ae4f40 commit e94d454

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

src/ng/rootScope.js

+56
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function $RootScopeProvider(){
7171
var TTL = 10;
7272
var $rootScopeMinErr = minErr('$rootScope');
7373
var lastDirtyWatch = null;
74+
var applyAsyncId = null;
7475

7576
this.digestTtl = function(value) {
7677
if (arguments.length) {
@@ -134,6 +135,7 @@ function $RootScopeProvider(){
134135
this.$$listeners = {};
135136
this.$$listenerCount = {};
136137
this.$$isolateBindings = {};
138+
this.$$applyAsyncQueue = [];
137139
}
138140

139141
/**
@@ -688,6 +690,13 @@ function $RootScopeProvider(){
688690

689691
beginPhase('$digest');
690692

693+
if (this === $rootScope && applyAsyncId !== null) {
694+
// If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
695+
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
696+
$browser.defer.cancel(applyAsyncId);
697+
flushApplyAsync();
698+
}
699+
691700
lastDirtyWatch = null;
692701

693702
do { // "while dirty" loop
@@ -997,6 +1006,33 @@ function $RootScopeProvider(){
9971006
}
9981007
},
9991008

1009+
/**
1010+
* @ngdoc method
1011+
* @name $rootScope.Scope#$applyAsync
1012+
* @kind function
1013+
*
1014+
* @description
1015+
* Schedule the invokation of $apply to occur at a later time. The actual time difference
1016+
* varies across browsers, but is typically around ~10 milliseconds.
1017+
*
1018+
* This can be used to queue up multiple expressions which need to be evaluated in the same
1019+
* digest.
1020+
*
1021+
* @param {(string|function())=} exp An angular expression to be executed.
1022+
*
1023+
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
1024+
* - `function(scope)`: execute the function with current `scope` parameter.
1025+
*/
1026+
$applyAsync: function(expr) {
1027+
var scope = this;
1028+
expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression);
1029+
scheduleApplyAsync();
1030+
1031+
function $applyAsyncExpression() {
1032+
scope.$eval(expr);
1033+
}
1034+
},
1035+
10001036
/**
10011037
* @ngdoc method
10021038
* @name $rootScope.Scope#$on
@@ -1229,5 +1265,25 @@ function $RootScopeProvider(){
12291265
* because it's unique we can easily tell it apart from other values
12301266
*/
12311267
function initWatchVal() {}
1268+
1269+
function flushApplyAsync() {
1270+
var queue = $rootScope.$$applyAsyncQueue;
1271+
while (queue.length) {
1272+
try {
1273+
queue.shift()();
1274+
} catch(e) {
1275+
$exceptionHandler(e);
1276+
}
1277+
}
1278+
applyAsyncId = null;
1279+
}
1280+
1281+
function scheduleApplyAsync() {
1282+
if (applyAsyncId === null) {
1283+
applyAsyncId = $browser.defer(function() {
1284+
$rootScope.$apply(flushApplyAsync);
1285+
});
1286+
}
1287+
}
12321288
}];
12331289
}

test/ng/rootScopeSpec.js

+85
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,91 @@ describe('Scope', function() {
13991399
});
14001400

14011401

1402+
describe('$applyAsync', function() {
1403+
beforeEach(module(function($exceptionHandlerProvider) {
1404+
$exceptionHandlerProvider.mode('log');
1405+
}));
1406+
1407+
1408+
it('should evaluate in the context of specific $scope', inject(function($rootScope, $browser) {
1409+
var scope = $rootScope.$new();
1410+
scope.$applyAsync('x = "CODE ORANGE"');
1411+
1412+
$browser.defer.flush();
1413+
expect(scope.x).toBe('CODE ORANGE');
1414+
expect($rootScope.x).toBeUndefined();
1415+
}));
1416+
1417+
1418+
it('should evaluate queued expressions in order', inject(function($rootScope, $browser) {
1419+
$rootScope.x = [];
1420+
$rootScope.$applyAsync('x.push("expr1")');
1421+
$rootScope.$applyAsync('x.push("expr2")');
1422+
1423+
$browser.defer.flush();
1424+
expect($rootScope.x).toEqual(['expr1', 'expr2']);
1425+
}));
1426+
1427+
1428+
it('should evaluate subsequently queued items in same turn', inject(function($rootScope, $browser) {
1429+
$rootScope.x = [];
1430+
$rootScope.$applyAsync(function() {
1431+
$rootScope.x.push('expr1');
1432+
$rootScope.$applyAsync('x.push("expr2")');
1433+
expect($browser.deferredFns.length).toBe(0);
1434+
});
1435+
1436+
$browser.defer.flush();
1437+
expect($rootScope.x).toEqual(['expr1', 'expr2']);
1438+
}));
1439+
1440+
1441+
it('should pass thrown exceptions to $exceptionHandler', inject(function($rootScope, $browser, $exceptionHandler) {
1442+
$rootScope.$applyAsync(function() {
1443+
throw 'OOPS';
1444+
});
1445+
1446+
$browser.defer.flush();
1447+
expect($exceptionHandler.errors).toEqual([
1448+
'OOPS'
1449+
]);
1450+
}));
1451+
1452+
1453+
it('should evaluate subsequent expressions after an exception is thrown', inject(function($rootScope, $browser) {
1454+
$rootScope.$applyAsync(function() {
1455+
throw 'OOPS';
1456+
});
1457+
$rootScope.$applyAsync('x = "All good!"');
1458+
1459+
$browser.defer.flush();
1460+
expect($rootScope.x).toBe('All good!');
1461+
}));
1462+
1463+
1464+
it('should be cancelled if a $rootScope digest occurs before the next tick', inject(function($rootScope, $browser) {
1465+
var apply = spyOn($rootScope, '$apply').andCallThrough();
1466+
var cancel = spyOn($browser.defer, 'cancel').andCallThrough();
1467+
var expression = jasmine.createSpy('expr');
1468+
1469+
$rootScope.$applyAsync(expression);
1470+
$rootScope.$digest();
1471+
expect(expression).toHaveBeenCalledOnce();
1472+
expect(cancel).toHaveBeenCalledOnce();
1473+
expression.reset();
1474+
cancel.reset();
1475+
1476+
// assert that we no longer are waiting to execute
1477+
expect($browser.deferredFns.length).toBe(0);
1478+
1479+
// assert that another digest won't call the function again
1480+
$rootScope.$digest();
1481+
expect(expression).not.toHaveBeenCalled();
1482+
expect(cancel).not.toHaveBeenCalled();
1483+
}));
1484+
});
1485+
1486+
14021487
describe('events', function() {
14031488

14041489
describe('$on', function() {

0 commit comments

Comments
 (0)