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

Commit efb822c

Browse files
petebacondarwinNarretz
authored andcommitted
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 #5301 Closes #16308
1 parent b7d1e0f commit efb822c

File tree

2 files changed

+218
-3
lines changed

2 files changed

+218
-3
lines changed

src/ng/rootScope.js

+97-3
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;
@@ -832,7 +834,7 @@ function $RootScopeProvider() {
832834

833835
traverseScopesLoop:
834836
do { // "traverse the scopes" loop
835-
if ((watchers = current.$$watchers)) {
837+
if ((watchers = !current.$$suspended && current.$$watchers)) {
836838
// process our watches
837839
watchers.$$digestWatchIndex = watchers.length;
838840
while (watchers.$$digestWatchIndex--) {
@@ -876,7 +878,9 @@ function $RootScopeProvider() {
876878
// Insanity Warning: scope depth-first traversal
877879
// yes, this code is a bit crazy, but it works and we have tests to prove it!
878880
// this piece should be kept in sync with the traversal in $broadcast
879-
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
881+
// (though it differs due to having the extra check for $$suspended and does not
882+
// check $$listenerCount)
883+
if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) ||
880884
(current !== target && current.$$nextSibling)))) {
881885
while (current !== target && !(next = current.$$nextSibling)) {
882886
current = current.$parent;
@@ -913,6 +917,95 @@ function $RootScopeProvider() {
913917
$browser.$$checkUrlChange();
914918
},
915919

920+
/**
921+
* @ngdoc method
922+
* @name $rootScope.Scope#$suspend
923+
* @kind function
924+
*
925+
* @description
926+
* Suspend watchers of this scope subtree so that they will not be invoked during digest.
927+
*
928+
* This can be used to optimize your application when you know that running those watchers
929+
* is redundant.
930+
*
931+
* **Warning**
932+
*
933+
* Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
934+
* Only use this approach if you are confident that you know what you are doing and have
935+
* ample tests to ensure that bindings get updated as you expect.
936+
*
937+
* Some of the things to consider are:
938+
*
939+
* * Any external event on a directive/component will not trigger a digest while the hosting
940+
* scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`.
941+
* * Transcluded content exists on a scope that inherits from outside a directive but exists
942+
* as a child of the directive's containing scope. If the containing scope is suspended the
943+
* transcluded scope will also be suspended, even if the scope from which the transcluded
944+
* scope inherits is not suspended.
945+
* * Multiple directives trying to manage the suspended status of a scope can confuse each other:
946+
* * A call to `$suspend()` on an already suspended scope is a no-op.
947+
* * A call to `$resume()` on a non-suspended scope is a no-op.
948+
* * If two directives suspend a scope, then one of them resumes the scope, the scope will no
949+
* longer be suspended. This could result in the other directive believing a scope to be
950+
* suspended when it is not.
951+
* * If a parent scope is suspended then all its descendants will be also excluded from future
952+
* digests whether or not they have been suspended themselves. Note that this also applies to
953+
* isolate child scopes.
954+
* * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers
955+
* for that scope and its descendants. When digesting we only check whether the current scope is
956+
* locally suspended, rather than checking whether it has a suspended ancestor.
957+
* * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be
958+
* included in future digests until all its ancestors have been resumed.
959+
* * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()`
960+
* against the `$rootScope` and so will still trigger a global digest even if the promise was
961+
* initiated by a component that lives on a suspended scope.
962+
*/
963+
$suspend: function() {
964+
this.$$suspended = true;
965+
},
966+
967+
/**
968+
* @ngdoc method
969+
* @name $rootScope.Scope#$isSuspended
970+
* @kind function
971+
*
972+
* @description
973+
* Call this method to determine if this scope has been explicitly suspended. It will not
974+
* tell you whether an ancestor has been suspended.
975+
* To determine if this scope will be excluded from a digest triggered at the $rootScope,
976+
* for example, you must check all its ancestors:
977+
*
978+
* ```
979+
* function isExcludedFromDigest(scope) {
980+
* while(scope) {
981+
* if (scope.$isSuspended()) return true;
982+
* scope = scope.$parent;
983+
* }
984+
* return false;
985+
* ```
986+
*
987+
* Be aware that a scope may not be included in digests if it has a suspended ancestor,
988+
* even if `$isSuspended()` returns false.
989+
*
990+
* @returns true if the current scope has been suspended.
991+
*/
992+
$isSuspended: function() {
993+
return this.$$suspended;
994+
},
995+
996+
/**
997+
* @ngdoc method
998+
* @name $rootScope.Scope#$resume
999+
* @kind function
1000+
*
1001+
* @description
1002+
* Resume watchers of this scope subtree in case it was suspended.
1003+
*
1004+
* See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach.
1005+
*/
1006+
$resume: function() {
1007+
this.$$suspended = false;
1008+
},
9161009

9171010
/**
9181011
* @ngdoc event
@@ -1348,7 +1441,8 @@ function $RootScopeProvider() {
13481441
// Insanity Warning: scope depth-first traversal
13491442
// yes, this code is a bit crazy, but it works and we have tests to prove it!
13501443
// this piece should be kept in sync with the traversal in $digest
1351-
// (though it differs due to having the extra check for $$listenerCount)
1444+
// (though it differs due to having the extra check for $$listenerCount and
1445+
// does not check $$suspended)
13521446
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
13531447
(current !== target && current.$$nextSibling)))) {
13541448
while (current !== target && !(next = current.$$nextSibling)) {

test/ng/rootScopeSpec.js

+121
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,127 @@ describe('Scope', function() {
932932
});
933933

934934

935+
describe('$suspend/$resume/$isSuspended', function() {
936+
it('should suspend watchers on scope', inject(function($rootScope) {
937+
var watchSpy = jasmine.createSpy('watchSpy');
938+
$rootScope.$watch(watchSpy);
939+
$rootScope.$suspend();
940+
$rootScope.$digest();
941+
expect(watchSpy).not.toHaveBeenCalled();
942+
}));
943+
944+
it('should resume watchers on scope', inject(function($rootScope) {
945+
var watchSpy = jasmine.createSpy('watchSpy');
946+
$rootScope.$watch(watchSpy);
947+
$rootScope.$suspend();
948+
$rootScope.$resume();
949+
$rootScope.$digest();
950+
expect(watchSpy).toHaveBeenCalled();
951+
}));
952+
953+
it('should suspend watchers on child scope', inject(function($rootScope) {
954+
var watchSpy = jasmine.createSpy('watchSpy');
955+
var scope = $rootScope.$new(true);
956+
scope.$watch(watchSpy);
957+
$rootScope.$suspend();
958+
$rootScope.$digest();
959+
expect(watchSpy).not.toHaveBeenCalled();
960+
}));
961+
962+
it('should resume watchers on child scope', inject(function($rootScope) {
963+
var watchSpy = jasmine.createSpy('watchSpy');
964+
var scope = $rootScope.$new(true);
965+
scope.$watch(watchSpy);
966+
$rootScope.$suspend();
967+
$rootScope.$resume();
968+
$rootScope.$digest();
969+
expect(watchSpy).toHaveBeenCalled();
970+
}));
971+
972+
it('should resume digesting immediately if `$resume` is called from an ancestor scope watch handler', inject(function($rootScope) {
973+
var watchSpy = jasmine.createSpy('watchSpy');
974+
var scope = $rootScope.$new();
975+
976+
// Setup a handler that will toggle the scope suspension
977+
$rootScope.$watch('a', function(a) { if (a) scope.$resume(); else scope.$suspend(); });
978+
979+
// Spy on the scope watches being called
980+
scope.$watch(watchSpy);
981+
982+
// Trigger a digest that should suspend the scope from within the watch handler
983+
$rootScope.$apply('a = false');
984+
// The scope is suspended before it gets to do a digest
985+
expect(watchSpy).not.toHaveBeenCalled();
986+
987+
// Trigger a digest that should resume the scope from within the watch handler
988+
$rootScope.$apply('a = true');
989+
// The watch handler that resumes the scope is in the parent, so the resumed scope will digest immediately
990+
expect(watchSpy).toHaveBeenCalled();
991+
}));
992+
993+
it('should resume digesting immediately if `$resume` is called from a non-ancestor scope watch handler', inject(function($rootScope) {
994+
var watchSpy = jasmine.createSpy('watchSpy');
995+
var scope = $rootScope.$new();
996+
var sibling = $rootScope.$new();
997+
998+
// Setup a handler that will toggle the scope suspension
999+
sibling.$watch('a', function(a) { if (a) scope.$resume(); else scope.$suspend(); });
1000+
1001+
// Spy on the scope watches being called
1002+
scope.$watch(watchSpy);
1003+
1004+
// Trigger a digest that should suspend the scope from within the watch handler
1005+
$rootScope.$apply('a = false');
1006+
// The scope is suspended by the sibling handler after the scope has already digested
1007+
expect(watchSpy).toHaveBeenCalled();
1008+
watchSpy.calls.reset();
1009+
1010+
// Trigger a digest that should resume the scope from within the watch handler
1011+
$rootScope.$apply('a = true');
1012+
// The watch handler that resumes the scope marks the digest as dirty, so it will run an extra digest
1013+
expect(watchSpy).toHaveBeenCalled();
1014+
}));
1015+
1016+
it('should not suspend watchers on parent or sibling scopes', inject(function($rootScope) {
1017+
var watchSpyParent = jasmine.createSpy('watchSpyParent');
1018+
var watchSpyChild = jasmine.createSpy('watchSpyChild');
1019+
var watchSpySibling = jasmine.createSpy('watchSpySibling');
1020+
1021+
var parent = $rootScope.$new();
1022+
parent.$watch(watchSpyParent);
1023+
var child = parent.$new();
1024+
child.$watch(watchSpyChild);
1025+
var sibling = parent.$new();
1026+
sibling.$watch(watchSpySibling);
1027+
1028+
child.$suspend();
1029+
$rootScope.$digest();
1030+
expect(watchSpyParent).toHaveBeenCalled();
1031+
expect(watchSpyChild).not.toHaveBeenCalled();
1032+
expect(watchSpySibling).toHaveBeenCalled();
1033+
}));
1034+
1035+
it('should return true from `$isSuspended()` when a scope is suspended', inject(function($rootScope) {
1036+
$rootScope.$suspend();
1037+
expect($rootScope.$isSuspended()).toBe(true);
1038+
$rootScope.$resume();
1039+
expect($rootScope.$isSuspended()).toBe(false);
1040+
}));
1041+
1042+
it('should return false from `$isSuspended()` for a non-suspended scope that has a suspended ancestor', inject(function($rootScope) {
1043+
var childScope = $rootScope.$new();
1044+
$rootScope.$suspend();
1045+
expect(childScope.$isSuspended()).toBe(false);
1046+
childScope.$suspend();
1047+
expect(childScope.$isSuspended()).toBe(true);
1048+
childScope.$resume();
1049+
expect(childScope.$isSuspended()).toBe(false);
1050+
$rootScope.$resume();
1051+
expect(childScope.$isSuspended()).toBe(false);
1052+
}));
1053+
});
1054+
1055+
9351056
describe('optimizations', function() {
9361057

9371058
function setupWatches(scope, log) {

0 commit comments

Comments
 (0)