aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKarl Seamon2013-12-10 17:50:30 -0500
committerIgor Minar2013-12-27 23:31:00 -0800
commit80e7a4558490f7ffd33d142844b9153a5ed00e86 (patch)
treea697bc0c7a440a86084dc1024f917ced98986066
parent498365f219f65d6c29bdf2f03610a4d3646009bb (diff)
downloadangular.js-80e7a4558490f7ffd33d142844b9153a5ed00e86.tar.bz2
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
-rw-r--r--src/ng/rootScope.js33
-rw-r--r--test/ng/rootScopeSpec.js126
2 files changed, 136 insertions, 23 deletions
diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js
index a56abc5c..1bb12869 100644
--- a/src/ng/rootScope.js
+++ b/src/ng/rootScope.js
@@ -133,6 +133,7 @@ function $RootScopeProvider(){
this.$$asyncQueue = [];
this.$$postDigestQueue = [];
this.$$listeners = {};
+ this.$$listenerCount = {};
this.$$isolateBindings = {};
}
@@ -192,6 +193,7 @@ function $RootScopeProvider(){
}
child['this'] = child;
child.$$listeners = {};
+ child.$$listenerCount = {};
child.$parent = this;
child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
child.$$prevSibling = this.$$childTail;
@@ -696,6 +698,8 @@ function $RootScopeProvider(){
this.$$destroyed = true;
if (this === $rootScope) return;
+ forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
+
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
@@ -885,8 +889,18 @@ function $RootScopeProvider(){
}
namedListeners.push(listener);
+ var current = this;
+ do {
+ if (!current.$$listenerCount[name]) {
+ current.$$listenerCount[name] = 0;
+ }
+ current.$$listenerCount[name]++;
+ } while ((current = current.$parent));
+
+ var self = this;
return function() {
namedListeners[indexOf(namedListeners, listener)] = null;
+ decrementListenerCount(self, 1, name);
};
},
@@ -998,8 +1012,7 @@ function $RootScopeProvider(){
listeners, i, length;
//down while you can, then up and next sibling or up and next sibling until back at root
- do {
- current = next;
+ while ((current = next)) {
event.currentScope = current;
listeners = current.$$listeners[name] || [];
for (i=0, length = listeners.length; i<length; i++) {
@@ -1021,12 +1034,14 @@ function $RootScopeProvider(){
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $digest
- if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
+ // (though it differs due to having the extra check for $$listenerCount)
+ if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
+ (current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
- } while ((current = next));
+ }
return event;
}
@@ -1055,6 +1070,16 @@ function $RootScopeProvider(){
return fn;
}
+ function decrementListenerCount(current, count, name) {
+ do {
+ current.$$listenerCount[name] -= count;
+
+ if (current.$$listenerCount[name] === 0) {
+ delete current.$$listenerCount[name];
+ }
+ } while ((current = current.$parent));
+ }
+
/**
* function used as an initial value for watchers.
* because it's unique we can easily tell it apart from other values
diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js
index cc6727c2..3677ccc8 100644
--- a/test/ng/rootScopeSpec.js
+++ b/test/ng/rootScopeSpec.js
@@ -730,6 +730,28 @@ describe('Scope', function() {
first.$apply();
expect(log).toBe('1232323');
}));
+
+
+ it('should decrement anscestor $$listenerCount entries', inject(function($rootScope) {
+ var EVENT = 'fooEvent',
+ spy = jasmine.createSpy('listener'),
+ firstSecond = first.$new();
+
+ firstSecond.$on(EVENT, spy);
+ firstSecond.$on(EVENT, spy);
+ middle.$on(EVENT, spy);
+
+ expect($rootScope.$$listenerCount[EVENT]).toBe(3);
+ expect(first.$$listenerCount[EVENT]).toBe(2);
+
+ firstSecond.$destroy();
+
+ expect($rootScope.$$listenerCount[EVENT]).toBe(1);
+ expect(first.$$listenerCount[EVENT]).toBeUndefined();
+
+ $rootScope.$broadcast(EVENT);
+ expect(spy.callCount).toBe(1);
+ }));
});
@@ -1091,29 +1113,78 @@ describe('Scope', function() {
}));
- it('should return a function that deregisters the listener', inject(function($rootScope) {
- var log = '',
- child = $rootScope.$new(),
- listenerRemove;
-
- function eventFn() {
- log += 'X';
- }
+ it('should increment ancestor $$listenerCount entries', inject(function($rootScope) {
+ var child1 = $rootScope.$new(),
+ child2 = child1.$new(),
+ spy = jasmine.createSpy();
- listenerRemove = child.$on('abc', eventFn);
- expect(log).toEqual('');
- expect(listenerRemove).toBeDefined();
+ $rootScope.$on('event1', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 1});
- child.$emit('abc');
- child.$broadcast('abc');
- expect(log).toEqual('XX');
+ child1.$on('event1', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 2});
+ expect(child1.$$listenerCount).toEqual({event1: 1});
- log = '';
- listenerRemove();
- child.$emit('abc');
- child.$broadcast('abc');
- expect(log).toEqual('');
+ child2.$on('event2', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 2, event2: 1});
+ expect(child1.$$listenerCount).toEqual({event1: 1, event2: 1});
+ expect(child2.$$listenerCount).toEqual({event2: 1});
}));
+
+
+ describe('deregistration', function() {
+
+ it('should return a function that deregisters the listener', inject(function($rootScope) {
+ var log = '',
+ child = $rootScope.$new(),
+ listenerRemove;
+
+ function eventFn() {
+ log += 'X';
+ }
+
+ listenerRemove = child.$on('abc', eventFn);
+ expect(log).toEqual('');
+ expect(listenerRemove).toBeDefined();
+
+ child.$emit('abc');
+ child.$broadcast('abc');
+ expect(log).toEqual('XX');
+ expect($rootScope.$$listenerCount['abc']).toBe(1);
+
+ log = '';
+ listenerRemove();
+ child.$emit('abc');
+ child.$broadcast('abc');
+ expect(log).toEqual('');
+ expect($rootScope.$$listenerCount['abc']).toBeUndefined();
+ }));
+
+
+ it('should decrement ancestor $$listenerCount entries', inject(function($rootScope) {
+ var child1 = $rootScope.$new(),
+ child2 = child1.$new(),
+ spy = jasmine.createSpy();
+
+ $rootScope.$on('event1', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 1});
+
+ child1.$on('event1', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 2});
+ expect(child1.$$listenerCount).toEqual({event1: 1});
+
+ var deregisterEvent2Listener = child2.$on('event2', spy);
+ expect($rootScope.$$listenerCount).toEqual({event1: 2, event2: 1});
+ expect(child1.$$listenerCount).toEqual({event1: 1, event2: 1});
+ expect(child2.$$listenerCount).toEqual({event2: 1});
+
+ deregisterEvent2Listener();
+
+ expect($rootScope.$$listenerCount).toEqual({event1: 2});
+ expect(child1.$$listenerCount).toEqual({event1: 1});
+ expect(child2.$$listenerCount).toEqual({});
+ }))
+ });
});
@@ -1360,6 +1431,23 @@ describe('Scope', function() {
}));
+ it('should not descend past scopes with a $$listerCount of 0 or undefined',
+ inject(function($rootScope) {
+ var EVENT = 'fooEvent',
+ spy = jasmine.createSpy('listener');
+
+ // Precondition: There should be no listeners for fooEvent.
+ expect($rootScope.$$listenerCount[EVENT]).toBeUndefined();
+
+ // Add a spy listener to a child scope.
+ $rootScope.$$childHead.$$listeners[EVENT] = [spy];
+
+ // $rootScope's count for 'fooEvent' is undefined, so spy should not be called.
+ $rootScope.$broadcast(EVENT);
+ expect(spy).not.toHaveBeenCalled();
+ }));
+
+
it('should return event object', function() {
var result = child1.$broadcast('some');