diff options
| author | Karl Seamon | 2013-12-10 17:50:30 -0500 |
|---|---|---|
| committer | Igor Minar | 2013-12-27 23:31:00 -0800 |
| commit | 80e7a4558490f7ffd33d142844b9153a5ed00e86 (patch) | |
| tree | a697bc0c7a440a86084dc1024f917ced98986066 | |
| parent | 498365f219f65d6c29bdf2f03610a4d3646009bb (diff) | |
| download | angular.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.js | 33 | ||||
| -rw-r--r-- | test/ng/rootScopeSpec.js | 126 |
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'); |
