Skip to content

Commit cee0dc0

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 817ac56 commit cee0dc0

File tree

2 files changed

+170
-2
lines changed

2 files changed

+170
-2
lines changed

src/ng/rootScope.js

+92-2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function $RootScopeProvider() {
9191
this.$$watchersCount = 0;
9292
this.$id = nextUid();
9393
this.$$ChildScope = null;
94+
this.$$suspended = false;
9495
}
9596
ChildScope.prototype = parent;
9697
return ChildScope;
@@ -178,6 +179,7 @@ function $RootScopeProvider() {
178179
this.$$childHead = this.$$childTail = null;
179180
this.$root = this;
180181
this.$$destroyed = false;
182+
this.$$suspended = false;
181183
this.$$listeners = {};
182184
this.$$listenerCount = {};
183185
this.$$watchersCount = 0;
@@ -811,7 +813,7 @@ function $RootScopeProvider() {
811813

812814
traverseScopesLoop:
813815
do { // "traverse the scopes" loop
814-
if ((watchers = current.$$watchers)) {
816+
if ((watchers = !current.$$suspended && current.$$watchers)) {
815817
// process our watches
816818
watchers.$$digestWatchIndex = watchers.length;
817819
while (watchers.$$digestWatchIndex--) {
@@ -855,7 +857,7 @@ function $RootScopeProvider() {
855857
// Insanity Warning: scope depth-first traversal
856858
// yes, this code is a bit crazy, but it works and we have tests to prove it!
857859
// this piece should be kept in sync with the traversal in $broadcast
858-
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
860+
if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) ||
859861
(current !== target && current.$$nextSibling)))) {
860862
while (current !== target && !(next = current.$$nextSibling)) {
861863
current = current.$parent;
@@ -892,6 +894,94 @@ function $RootScopeProvider() {
892894
$browser.$$checkUrlChange();
893895
},
894896

897+
/**
898+
* @ngdoc method
899+
* @name $rootScope.Scope#$suspend
900+
* @kind function
901+
*
902+
* @description
903+
* Suspend watchers of this scope subtree so that they will not be invoked during digest.
904+
*
905+
* This can be used to optimize your application when you know that running those watchers
906+
* is redundant.
907+
*
908+
* **Warning**
909+
*
910+
* Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
911+
* Only use this approach if you are confident that you know what you are doing and have
912+
* ample tests to ensure that bindings get updated as you expect.
913+
*
914+
* Some of the things to consider are:
915+
*
916+
* * Any external event on a directive/component will not trigger a digest while the hosting
917+
* scope is suspended - even if the event handler calls `$apply` or `$digest`.
918+
* * Transcluded content exists on a scope that inherits from outside a directive but exists
919+
* as a child of the directive's containing scope. If the containing scope is suspended the
920+
* transcluded scope will also be suspended, even if the scope from which the transcluded
921+
* scope inherits is not suspended.
922+
* * Multiple directives trying to manage the suspended status of a scope can confuse each other:
923+
* * A call to `$suspend` an already suspended scope is a no-op.
924+
* * A call to `$resume` a non-suspended scope is a no-op.
925+
* * If two directives suspend a scope, then one of them resumes the scope, the scope will no
926+
* longer be suspended. This could result in the other directive believing a scope to be
927+
* suspended when it is not.
928+
* * If a parent scope is suspended then all its descendants will be excluded from future digests
929+
* whether or not they have been suspended themselves. Note that this also applies to isolate
930+
* child scopes.
931+
* * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be
932+
* included in future digests until all its ancestors have been resumed.
933+
* * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply`
934+
* against the `$rootScope` and so will still trigger a global digest even if the promise was
935+
* initialized by a component that lives on a suspended scope.
936+
*/
937+
$suspend: function() {
938+
this.$$suspended = true;
939+
},
940+
941+
/**
942+
* @ngdoc method
943+
* @name $rootScope.Scope#$isSuspended
944+
* @kind function
945+
*
946+
* @description
947+
* Call this method to determine if this scope or one of its ancestors have been suspended.
948+
*
949+
* Be aware that a scope may not be included in digests if it has a suspended ancestor,
950+
* even if `$isSuspended()` returns false.
951+
*
952+
* To determine if this scope will be excluded in digests then you must check all its ancestors:
953+
*
954+
* ```
955+
* function isExcludedFromDigest(scope) {
956+
* while(scope) {
957+
* if (scope.$isSuspended()) return true;
958+
* scope = scope.$parent;
959+
* }
960+
* return false;
961+
* ```
962+
*
963+
* @returns true if the current scope has been suspended.
964+
*/
965+
$isSuspended: function() {
966+
return this.$$suspended;
967+
},
968+
969+
/**
970+
* @ngdoc method
971+
* @name $rootScope.Scope#$resume
972+
* @kind function
973+
*
974+
* @description
975+
* Resume watchers of this scope subtree in case it was suspended.
976+
*
977+
* It is recommended to digest on this scope after it is resumed to catch any modifications
978+
* that might have happened while it was suspended.
979+
*
980+
* See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach.
981+
*/
982+
$resume: function() {
983+
this.$$suspended = false;
984+
},
895985

896986
/**
897987
* @ngdoc event

test/ng/rootScopeSpec.js

+78
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,84 @@ 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 resume watchers on child scope', inject(function($rootScope) {
1287+
var watchSpy = jasmine.createSpy('watchSpy');
1288+
var scope = $rootScope.$new(true);
1289+
scope.$watch(watchSpy);
1290+
$rootScope.$suspend();
1291+
$rootScope.$resume();
1292+
$rootScope.$digest();
1293+
expect(watchSpy).toHaveBeenCalled();
1294+
}));
1295+
1296+
it('should not suspend watchers on parent or sibling scopes', inject(function($rootScope) {
1297+
var watchSpyParent = jasmine.createSpy('watchSpyParent');
1298+
var watchSpyChild = jasmine.createSpy('watchSpyChild');
1299+
var watchSpySibling = jasmine.createSpy('watchSpySibling');
1300+
1301+
var parent = $rootScope.$new();
1302+
parent.$watch(watchSpyParent);
1303+
var child = parent.$new();
1304+
child.$watch(watchSpyChild);
1305+
var sibling = parent.$new();
1306+
sibling.$watch(watchSpySibling);
1307+
1308+
child.$suspend();
1309+
$rootScope.$digest();
1310+
expect(watchSpyParent).toHaveBeenCalled();
1311+
expect(watchSpyChild).not.toHaveBeenCalled();
1312+
expect(watchSpySibling).toHaveBeenCalled();
1313+
}));
1314+
1315+
it('should return true from `$isSuspended()` when a scope is suspended', inject(function($rootScope) {
1316+
$rootScope.$suspend();
1317+
expect($rootScope.$isSuspended()).toBe(true);
1318+
$rootScope.$resume();
1319+
expect($rootScope.$isSuspended()).toBe(false);
1320+
}));
1321+
1322+
it('should return false from `$isSuspended()` for a non-suspended scope that has a suspended ancestor', inject(function($rootScope) {
1323+
var childScope = $rootScope.$new();
1324+
$rootScope.$suspend();
1325+
expect(childScope.$isSuspended()).toBe(false);
1326+
childScope.$suspend();
1327+
expect(childScope.$isSuspended()).toBe(true);
1328+
childScope.$resume();
1329+
expect(childScope.$isSuspended()).toBe(false);
1330+
$rootScope.$resume();
1331+
expect(childScope.$isSuspended()).toBe(false);
1332+
}));
1333+
});
1334+
1335+
12581336
describe('optimizations', function() {
12591337

12601338
function setupWatches(scope, log) {

0 commit comments

Comments
 (0)