Skip to content

Commit bd90561

Browse files
committed
perf(Scope): limit propagation of $broadcast to scopes that have registered 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. Resolves angular#5341
1 parent 043190f commit bd90561

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

src/ng/rootScope.js

+30-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, 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.call(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,17 @@ function $RootScopeProvider(){
10551070
return fn;
10561071
}
10571072

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

test/ng/rootScopeSpec.js

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

735756

@@ -1107,13 +1128,34 @@ describe('Scope', function() {
11071128
child.$emit('abc');
11081129
child.$broadcast('abc');
11091130
expect(log).toEqual('XX');
1131+
expect($rootScope.$$listenerCount['abc']).toBe(1);
11101132

11111133
log = '';
11121134
listenerRemove();
11131135
child.$emit('abc');
11141136
child.$broadcast('abc');
11151137
expect(log).toEqual('');
1138+
expect($rootScope.$$listenerCount['abc']).toBeUndefined();
11161139
}));
1140+
1141+
1142+
it('should increment ancestor $$listenerCount entries', inject(function($rootScope) {
1143+
var child1 = $rootScope.$new(),
1144+
child2 = child1.$new(),
1145+
spy = jasmine.createSpy();
1146+
1147+
$rootScope.$on('event1', spy);
1148+
expect($rootScope.$$listenerCount).toEqual({event1: 1});
1149+
1150+
child1.$on('event1', spy);
1151+
expect($rootScope.$$listenerCount).toEqual({event1: 2});
1152+
expect(child1.$$listenerCount).toEqual({event1: 1});
1153+
1154+
child2.$on('event2', spy);
1155+
expect($rootScope.$$listenerCount).toEqual({event1: 2, event2: 1});
1156+
expect(child1.$$listenerCount).toEqual({event1: 1, event2: 1});
1157+
expect(child2.$$listenerCount).toEqual({event2: 1});
1158+
}))
11171159
});
11181160

11191161

@@ -1358,6 +1400,23 @@ describe('Scope', function() {
13581400
$rootScope.$broadcast('fooEvent');
13591401
expect(log).toBe('');
13601402
}));
1403+
1404+
1405+
it('should not descend past scopes with a $$listerCount of 0 or undefined',
1406+
inject(function($rootScope) {
1407+
var EVENT = 'fooEvent',
1408+
spy = jasmine.createSpy('listener');
1409+
1410+
// Precondition: There should be no listeners for fooEvent.
1411+
expect($rootScope.$$listenerCount[EVENT]).toBeUndefined();
1412+
1413+
// Add a spy listener to a child scope.
1414+
$rootScope.$$childHead.$$listeners[EVENT] = [spy];
1415+
1416+
// $rootScope's count for 'fooEvent' is undefined, so spy should not be called.
1417+
$rootScope.$broadcast(EVENT);
1418+
expect(spy).not.toHaveBeenCalled();
1419+
}));
13611420

13621421

13631422
it('should return event object', function() {

0 commit comments

Comments
 (0)