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

Commit 1b39756

Browse files
committed
feat($rootScope): implement $$applyAsync to support coalesced $apply calls
1 parent d713ad1 commit 1b39756

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/ng/rootScope.js

+37
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,12 @@ function $RootScopeProvider(){
688690

689691
beginPhase('$digest');
690692

693+
if (this === $rootScope && applyAsyncId !== null) {
694+
// If there was an $$applyAsync in progress, clear it.
695+
$browser.defer.cancel(applyAsyncId);
696+
completeApplyAsync();
697+
}
698+
691699
lastDirtyWatch = null;
692700

693701
do { // "while dirty" loop
@@ -997,6 +1005,11 @@ function $RootScopeProvider(){
9971005
}
9981006
},
9991007

1008+
$$applyAsync: function(expr, locals) {
1009+
expr && $rootScope.$$applyAsyncQueue.push(bind(this, this.$eval, expr, locals));
1010+
scheduleApplyAsync();
1011+
},
1012+
10001013
/**
10011014
* @ngdoc method
10021015
* @name $rootScope.Scope#$on
@@ -1229,5 +1242,29 @@ function $RootScopeProvider(){
12291242
* because it's unique we can easily tell it apart from other values
12301243
*/
12311244
function initWatchVal() {}
1245+
1246+
function tryCatch(fn) {
1247+
try {
1248+
fn();
1249+
} catch(e) {
1250+
$exceptionHandler(e);
1251+
}
1252+
}
1253+
1254+
function completeApplyAsync() {
1255+
applyAsyncId = null;
1256+
var queue = $rootScope.$$applyAsyncQueue;
1257+
while (queue.length) {
1258+
tryCatch(queue.shift());
1259+
}
1260+
}
1261+
1262+
function scheduleApplyAsync() {
1263+
if (applyAsyncId === null) {
1264+
applyAsyncId = $browser.defer(function() {
1265+
$rootScope.$apply(completeApplyAsync);
1266+
});
1267+
}
1268+
}
12321269
}];
12331270
}

test/ng/rootScopeSpec.js

+93
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,99 @@ 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 allow use of locals', inject(function($rootScope, $browser) {
1419+
$rootScope.$$applyAsync('x = $mode', { $mode: 'DEFERRED' });
1420+
1421+
$browser.defer.flush();
1422+
expect($rootScope.x).toBe('DEFERRED');
1423+
}));
1424+
1425+
1426+
it('should evaluate queued expressions in order', inject(function($rootScope, $browser) {
1427+
$rootScope.x = [];
1428+
$rootScope.$$applyAsync('x.push("expr1")');
1429+
$rootScope.$$applyAsync('x.push("expr2")');
1430+
1431+
$browser.defer.flush();
1432+
expect($rootScope.x).toEqual(['expr1', 'expr2']);
1433+
}));
1434+
1435+
1436+
it('should evaluate subsequently queued items in same turn', inject(function($rootScope, $browser) {
1437+
$rootScope.x = [];
1438+
$rootScope.$$applyAsync(function() {
1439+
$rootScope.x.push('expr1');
1440+
$rootScope.$$applyAsync('x.push("expr2")');
1441+
});
1442+
1443+
$browser.defer.flush();
1444+
expect($rootScope.x).toEqual(['expr1', 'expr2']);
1445+
expect($browser.deferredFns.length).toBe(0);
1446+
}));
1447+
1448+
1449+
it('should pass thrown exceptions to $exceptionHandler', inject(function($rootScope, $browser, $exceptionHandler) {
1450+
$rootScope.$$applyAsync(function() {
1451+
throw 'OOPS';
1452+
});
1453+
1454+
$browser.defer.flush();
1455+
expect($exceptionHandler.errors).toEqual([
1456+
'OOPS'
1457+
]);
1458+
}));
1459+
1460+
1461+
it('should evaluate subsequent expressions after an exception is thrown', inject(function($rootScope, $browser) {
1462+
$rootScope.$$applyAsync(function() {
1463+
throw 'OOPS';
1464+
});
1465+
$rootScope.$$applyAsync('x = "All good!"');
1466+
1467+
$browser.defer.flush();
1468+
expect($rootScope.x).toBe('All good!');
1469+
}));
1470+
1471+
1472+
it('should be cancelled if a $rootScope digest occurs before the next tick', inject(function($rootScope, $browser) {
1473+
var apply = spyOn($rootScope, '$apply').andCallThrough();
1474+
var cancel = spyOn($browser.defer, 'cancel').andCallThrough();
1475+
var expression = jasmine.createSpy('expr');
1476+
1477+
$rootScope.$$applyAsync(expression);
1478+
$rootScope.$digest();
1479+
expect(expression).toHaveBeenCalledOnce();
1480+
expect(cancel).toHaveBeenCalledOnce();
1481+
expression.reset();
1482+
cancel.reset();
1483+
1484+
// assert that we no longer are waiting to execute
1485+
expect($browser.deferredFns.length).toBe(0);
1486+
1487+
// assert that another digest won't call the function again
1488+
$rootScope.$digest();
1489+
expect(expression).not.toHaveBeenCalled();
1490+
expect(cancel).not.toHaveBeenCalled();
1491+
}));
1492+
});
1493+
1494+
14021495
describe('events', function() {
14031496

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

0 commit comments

Comments
 (0)