Skip to content

Commit 4951709

Browse files
feat($rootScope): allow suspending and resuming watchers on scope
This can be very helpful for external modules that help making the digest loop faster by ignoring some of the watchers under some circumstance. Example: https://github.com/shahata/angular-viewport-watch Thanks to @shahata for the original implementation. Closes angular#5301
1 parent 129662c commit 4951709

File tree

2 files changed

+104
-2
lines changed

2 files changed

+104
-2
lines changed

src/ng/rootScope.js

+55-2
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function $RootScopeProvider() {
178178
this.$$childHead = this.$$childTail = null;
179179
this.$root = this;
180180
this.$$destroyed = false;
181+
this.$$suspended = false;
181182
this.$$listeners = {};
182183
this.$$listenerCount = {};
183184
this.$$watchersCount = 0;
@@ -811,7 +812,7 @@ function $RootScopeProvider() {
811812

812813
traverseScopesLoop:
813814
do { // "traverse the scopes" loop
814-
if ((watchers = current.$$watchers)) {
815+
if ((watchers = !current.$$suspended && current.$$watchers)) {
815816
// process our watches
816817
watchers.$$digestWatchIndex = watchers.length;
817818
while (watchers.$$digestWatchIndex--) {
@@ -855,7 +856,7 @@ function $RootScopeProvider() {
855856
// Insanity Warning: scope depth-first traversal
856857
// yes, this code is a bit crazy, but it works and we have tests to prove it!
857858
// this piece should be kept in sync with the traversal in $broadcast
858-
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
859+
if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) ||
859860
(current !== target && current.$$nextSibling)))) {
860861
while (current !== target && !(next = current.$$nextSibling)) {
861862
current = current.$parent;
@@ -892,6 +893,58 @@ function $RootScopeProvider() {
892893
$browser.$$checkUrlChange();
893894
},
894895

896+
/**
897+
* @ngdoc method
898+
* @name $rootScope.Scope#$suspend
899+
* @kind function
900+
*
901+
* @description
902+
* Suspend watchers of this scope subtree so that they will not be invoked during digest.
903+
*
904+
* This can be used to optimize your application when you know that running those watchers
905+
* is redundant.
906+
*
907+
* **Warning**
908+
*
909+
* Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
910+
* Only use this approach if you are confident that you know what you are doing and have
911+
* ample tests to ensure that bindings get updated as you expect.
912+
*
913+
* Some of the things to consider are:
914+
*
915+
* * Any external event on a directive/component will not trigger a digest while the hosting
916+
* scope is suspended - even if the event handler calls `$apply` or `$digest`.
917+
* * Transcluded content exists on a scope that inherits from outside a directive but exists
918+
* as a child of the directive's containing scope. If the containing scope is suspended the
919+
* transcluded scope will also be suspended, even if the scope from which the transcluded
920+
* scope inherits is not suspended.
921+
* * Multiple directives trying to manage the suspended status of a scope can confuse each other:
922+
* * A call to `$suspend` an already suspended scope is a no-op.
923+
* * A call to `$resume` a non-suspended scope is a no-op.
924+
* * If two directives suspend a scope, then one of them resumes the scope, the scope will no
925+
* longer be suspended. This could result in the other directive believing a scope to be
926+
* suspended when it is not.
927+
*/
928+
$suspend: function() {
929+
this.$$suspended = true;
930+
},
931+
932+
/**
933+
* @ngdoc method
934+
* @name $rootScope.Scope#$resume
935+
* @kind function
936+
*
937+
* @description
938+
* Resume watchers of this scope subtree in case it was suspended.
939+
*
940+
* It is recommended to digest on this scope after it is resumed to catch any modifications
941+
* that might have happened while it was suspended.
942+
*
943+
* See {@link $rootScope.Scope.$suspend} for information about the dangers of using this approach.
944+
*/
945+
$resume: function() {
946+
this.$$suspended = false;
947+
},
895948

896949
/**
897950
* @ngdoc event

test/ng/rootScopeSpec.js

+49
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,55 @@ describe('Scope', function() {
12551255
});
12561256
});
12571257

1258+
1259+
describe('$suspend/$resume', function() {
1260+
it('should suspend watchers on scope', inject(function($rootScope) {
1261+
var watchSpy = jasmine.createSpy('watchSpy');
1262+
$rootScope.$watch(watchSpy);
1263+
$rootScope.$suspend();
1264+
$rootScope.$digest();
1265+
expect(watchSpy).not.toHaveBeenCalled();
1266+
}));
1267+
1268+
it('should resume watchers on scope', inject(function($rootScope) {
1269+
var watchSpy = jasmine.createSpy('watchSpy');
1270+
$rootScope.$watch(watchSpy);
1271+
$rootScope.$suspend();
1272+
$rootScope.$resume();
1273+
$rootScope.$digest();
1274+
expect(watchSpy).toHaveBeenCalled();
1275+
}));
1276+
1277+
it('should suspend watchers on child scope', inject(function($rootScope) {
1278+
var watchSpy = jasmine.createSpy('watchSpy');
1279+
var scope = $rootScope.$new(true);
1280+
scope.$watch(watchSpy);
1281+
$rootScope.$suspend();
1282+
$rootScope.$digest();
1283+
expect(watchSpy).not.toHaveBeenCalled();
1284+
}));
1285+
1286+
it('should not suspend watchers on parent or sibling scopes', inject(function($rootScope) {
1287+
var watchSpyParent = jasmine.createSpy('watchSpyParent');
1288+
var watchSpyChild = jasmine.createSpy('watchSpyChild');
1289+
var watchSpySibling = jasmine.createSpy('watchSpySibling');
1290+
1291+
var parent = $rootScope.$new();
1292+
parent.$watch(watchSpyParent);
1293+
var child = parent.$new();
1294+
child.$watch(watchSpyChild);
1295+
var sibling = parent.$new();
1296+
sibling.$watch(watchSpySibling);
1297+
1298+
child.$suspend();
1299+
$rootScope.$digest();
1300+
expect(watchSpyParent).toHaveBeenCalled();
1301+
expect(watchSpyChild).not.toHaveBeenCalled();
1302+
expect(watchSpySibling).toHaveBeenCalled();
1303+
}));
1304+
});
1305+
1306+
12581307
describe('optimizations', function() {
12591308

12601309
function setupWatches(scope, log) {

0 commit comments

Comments
 (0)