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

Commit 80e7a45

Browse files
kseamonIgorMinar
authored andcommitted
perf(Scope): limit propagation of $broadcast to scopes that have listeners for the event
Update $on and $destroy to maintain a count of event keys registered for each scope and its children. $broadcast will not descend past a node that has a count of 0/undefined for the $broadcasted event key. Closes #5341 Closes #5371
1 parent 498365f commit 80e7a45

File tree

2 files changed

+136
-23
lines changed

2 files changed

+136
-23
lines changed

src/ng/rootScope.js

+29-4
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function $RootScopeProvider(){
133133
this.$$asyncQueue = [];
134134
this.$$postDigestQueue = [];
135135
this.$$listeners = {};
136+
this.$$listenerCount = {};
136137
this.$$isolateBindings = {};
137138
}
138139

@@ -192,6 +193,7 @@ function $RootScopeProvider(){
192193
}
193194
child['this'] = child;
194195
child.$$listeners = {};
196+
child.$$listenerCount = {};
195197
child.$parent = this;
196198
child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
197199
child.$$prevSibling = this.$$childTail;
@@ -696,6 +698,8 @@ function $RootScopeProvider(){
696698
this.$$destroyed = true;
697699
if (this === $rootScope) return;
698700

701+
forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
702+
699703
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
700704
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
701705
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
@@ -885,8 +889,18 @@ function $RootScopeProvider(){
885889
}
886890
namedListeners.push(listener);
887891

892+
var current = this;
893+
do {
894+
if (!current.$$listenerCount[name]) {
895+
current.$$listenerCount[name] = 0;
896+
}
897+
current.$$listenerCount[name]++;
898+
} while ((current = current.$parent));
899+
900+
var self = this;
888901
return function() {
889902
namedListeners[indexOf(namedListeners, listener)] = null;
903+
decrementListenerCount(self, 1, name);
890904
};
891905
},
892906

@@ -998,8 +1012,7 @@ function $RootScopeProvider(){
9981012
listeners, i, length;
9991013

10001014
//down while you can, then up and next sibling or up and next sibling until back at root
1001-
do {
1002-
current = next;
1015+
while ((current = next)) {
10031016
event.currentScope = current;
10041017
listeners = current.$$listeners[name] || [];
10051018
for (i=0, length = listeners.length; i<length; i++) {
@@ -1021,12 +1034,14 @@ function $RootScopeProvider(){
10211034
// Insanity Warning: scope depth-first traversal
10221035
// yes, this code is a bit crazy, but it works and we have tests to prove it!
10231036
// this piece should be kept in sync with the traversal in $digest
1024-
if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
1037+
// (though it differs due to having the extra check for $$listenerCount)
1038+
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
1039+
(current !== target && current.$$nextSibling)))) {
10251040
while(current !== target && !(next = current.$$nextSibling)) {
10261041
current = current.$parent;
10271042
}
10281043
}
1029-
} while ((current = next));
1044+
}
10301045

10311046
return event;
10321047
}
@@ -1055,6 +1070,16 @@ function $RootScopeProvider(){
10551070
return fn;
10561071
}
10571072

1073+
function decrementListenerCount(current, count, name) {
1074+
do {
1075+
current.$$listenerCount[name] -= count;
1076+
1077+
if (current.$$listenerCount[name] === 0) {
1078+
delete current.$$listenerCount[name];
1079+
}
1080+
} while ((current = current.$parent));
1081+
}
1082+
10581083
/**
10591084
* function used as an initial value for watchers.
10601085
* because it's unique we can easily tell it apart from other values

test/ng/rootScopeSpec.js

+107-19
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,28 @@ describe('Scope', function() {
730730
first.$apply();
731731
expect(log).toBe('1232323');
732732
}));
733+
734+
735+
it('should decrement anscestor $$listenerCount entries', inject(function($rootScope) {
736+
var EVENT = 'fooEvent',
737+
spy = jasmine.createSpy('listener'),
738+
firstSecond = first.$new();
739+
740+
firstSecond.$on(EVENT, spy);
741+
firstSecond.$on(EVENT, spy);
742+
middle.$on(EVENT, spy);
743+
744+
expect($rootScope.$$listenerCount[EVENT]).toBe(3);
745+
expect(first.$$listenerCount[EVENT]).toBe(2);
746+
747+
firstSecond.$destroy();
748+
749+
expect($rootScope.$$listenerCount[EVENT]).toBe(1);
750+
expect(first.$$listenerCount[EVENT]).toBeUndefined();
751+
752+
$rootScope.$broadcast(EVENT);
753+
expect(spy.callCount).toBe(1);
754+
}));
733755
});
734756

735757

@@ -1091,29 +1113,78 @@ describe('Scope', function() {
10911113
}));
10921114

10931115

1094-
it('should return a function that deregisters the listener', inject(function($rootScope) {
1095-
var log = '',
1096-
child = $rootScope.$new(),
1097-
listenerRemove;
1098-
1099-
function eventFn() {
1100-
log += 'X';
1101-
}
1116+
it('should increment ancestor $$listenerCount entries', inject(function($rootScope) {
1117+
var child1 = $rootScope.$new(),
1118+
child2 = child1.$new(),
1119+
spy = jasmine.createSpy();
11021120

1103-
listenerRemove = child.$on('abc', eventFn);
1104-
expect(log).toEqual('');
1105-
expect(listenerRemove).toBeDefined();
1121+
$rootScope.$on('event1', spy);
1122+
expect($rootScope.$$listenerCount).toEqual({event1: 1});
11061123

1107-
child.$emit('abc');
1108-
child.$broadcast('abc');
1109-
expect(log).toEqual('XX');
1124+
child1.$on('event1', spy);
1125+
expect($rootScope.$$listenerCount).toEqual({event1: 2});
1126+
expect(child1.$$listenerCount).toEqual({event1: 1});
11101127

1111-
log = '';
1112-
listenerRemove();
1113-
child.$emit('abc');
1114-
child.$broadcast('abc');
1115-
expect(log).toEqual('');
1128+
child2.$on('event2', spy);
1129+
expect($rootScope.$$listenerCount).toEqual({event1: 2, event2: 1});
1130+
expect(child1.$$listenerCount).toEqual({event1: 1, event2: 1});
1131+
expect(child2.$$listenerCount).toEqual({event2: 1});
11161132
}));
1133+
1134+
1135+
describe('deregistration', function() {
1136+
1137+
it('should return a function that deregisters the listener', inject(function($rootScope) {
1138+
var log = '',
1139+
child = $rootScope.$new(),
1140+
listenerRemove;
1141+
1142+
function eventFn() {
1143+
log += 'X';
1144+
}
1145+
1146+
listenerRemove = child.$on('abc', eventFn);
1147+
expect(log).toEqual('');
1148+
expect(listenerRemove).toBeDefined();
1149+
1150+
child.$emit('abc');
1151+
child.$broadcast('abc');
1152+
expect(log).toEqual('XX');
1153+
expect($rootScope.$$listenerCount['abc']).toBe(1);
1154+
1155+
log = '';
1156+
listenerRemove();
1157+
child.$emit('abc');
1158+
child.$broadcast('abc');
1159+
expect(log).toEqual('');
1160+
expect($rootScope.$$listenerCount['abc']).toBeUndefined();
1161+
}));
1162+
1163+
1164+
it('should decrement ancestor $$listenerCount entries', inject(function($rootScope) {
1165+
var child1 = $rootScope.$new(),
1166+
child2 = child1.$new(),
1167+
spy = jasmine.createSpy();
1168+
1169+
$rootScope.$on('event1', spy);
1170+
expect($rootScope.$$listenerCount).toEqual({event1: 1});
1171+
1172+
child1.$on('event1', spy);
1173+
expect($rootScope.$$listenerCount).toEqual({event1: 2});
1174+
expect(child1.$$listenerCount).toEqual({event1: 1});
1175+
1176+
var deregisterEvent2Listener = child2.$on('event2', spy);
1177+
expect($rootScope.$$listenerCount).toEqual({event1: 2, event2: 1});
1178+
expect(child1.$$listenerCount).toEqual({event1: 1, event2: 1});
1179+
expect(child2.$$listenerCount).toEqual({event2: 1});
1180+
1181+
deregisterEvent2Listener();
1182+
1183+
expect($rootScope.$$listenerCount).toEqual({event1: 2});
1184+
expect(child1.$$listenerCount).toEqual({event1: 1});
1185+
expect(child2.$$listenerCount).toEqual({});
1186+
}))
1187+
});
11171188
});
11181189

11191190

@@ -1360,6 +1431,23 @@ describe('Scope', function() {
13601431
}));
13611432

13621433

1434+
it('should not descend past scopes with a $$listerCount of 0 or undefined',
1435+
inject(function($rootScope) {
1436+
var EVENT = 'fooEvent',
1437+
spy = jasmine.createSpy('listener');
1438+
1439+
// Precondition: There should be no listeners for fooEvent.
1440+
expect($rootScope.$$listenerCount[EVENT]).toBeUndefined();
1441+
1442+
// Add a spy listener to a child scope.
1443+
$rootScope.$$childHead.$$listeners[EVENT] = [spy];
1444+
1445+
// $rootScope's count for 'fooEvent' is undefined, so spy should not be called.
1446+
$rootScope.$broadcast(EVENT);
1447+
expect(spy).not.toHaveBeenCalled();
1448+
}));
1449+
1450+
13631451
it('should return event object', function() {
13641452
var result = child1.$broadcast('some');
13651453

0 commit comments

Comments
 (0)