Skip to content

Commit fd08be9

Browse files
Merge pull request #28 from angular/master
Update upstream
2 parents cef1dad + 2c9c3a0 commit fd08be9

File tree

4 files changed

+176
-50
lines changed

4 files changed

+176
-50
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@ngdoc error
2+
@name $rootScope:inevt
3+
@fullName Recursive $emit/$broadcast event
4+
@description
5+
6+
This error occurs when the an event is `$emit`ed or `$broadcast`ed recursively on a scope.
7+
8+
For example, when an event listener fires the same event being listened to.
9+
10+
```
11+
$scope.$on('foo', function() {
12+
$scope.$emit('foo');
13+
});
14+
```
15+
16+
Or when a parent element causes indirect recursion.
17+
18+
```
19+
$scope.$on('foo', function() {
20+
$rootScope.$broadcast('foo');
21+
});
22+
```

docs/content/guide/unit-testing.ngdoc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,17 @@ for instantiating controllers.
149149
describe('PasswordController', function() {
150150
beforeEach(module('app'));
151151

152-
var $controller;
152+
var $controller, $rootScope;
153153

154-
beforeEach(inject(function(_$controller_){
154+
beforeEach(inject(function(_$controller_, _$rootScope_){
155155
// The injector unwraps the underscores (_) from around the parameter names when matching
156156
$controller = _$controller_;
157+
$rootScope = _$rootScope_;
157158
}));
158159

159160
describe('$scope.grade', function() {
160161
it('sets the strength to "strong" if the password length is >8 chars', function() {
161-
var $scope = {};
162+
var $scope = $rootScope.$new();
162163
var controller = $controller('PasswordController', { $scope: $scope });
163164
$scope.password = 'longerthaneightchars';
164165
$scope.grade();

src/ng/rootScope.js

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,10 +1181,14 @@ function $RootScopeProvider() {
11811181

11821182
var self = this;
11831183
return function() {
1184-
var indexOfListener = namedListeners.indexOf(listener);
1185-
if (indexOfListener !== -1) {
1186-
namedListeners[indexOfListener] = null;
1184+
var index = arrayRemove(namedListeners, listener);
1185+
if (index >= 0) {
11871186
decrementListenerCount(self, 1, name);
1187+
// We are removing a listener while iterating over the list of listeners.
1188+
// Update the current $$index if necessary to ensure no listener is skipped.
1189+
if (index <= namedListeners.$$index) {
1190+
namedListeners.$$index--;
1191+
}
11881192
}
11891193
};
11901194
},
@@ -1213,9 +1217,7 @@ function $RootScopeProvider() {
12131217
* @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}).
12141218
*/
12151219
$emit: function(name, args) {
1216-
var empty = [],
1217-
namedListeners,
1218-
scope = this,
1220+
var scope = this,
12191221
stopPropagation = false,
12201222
event = {
12211223
name: name,
@@ -1226,28 +1228,11 @@ function $RootScopeProvider() {
12261228
},
12271229
defaultPrevented: false
12281230
},
1229-
listenerArgs = concat([event], arguments, 1),
1230-
i, length;
1231+
listenerArgs = concat([event], arguments, 1);
12311232

12321233
do {
1233-
namedListeners = scope.$$listeners[name] || empty;
1234-
event.currentScope = scope;
1235-
for (i = 0, length = namedListeners.length; i < length; i++) {
1236-
1237-
// if listeners were deregistered, defragment the array
1238-
if (!namedListeners[i]) {
1239-
namedListeners.splice(i, 1);
1240-
i--;
1241-
length--;
1242-
continue;
1243-
}
1244-
try {
1245-
//allow all listeners attached to the current scope to run
1246-
namedListeners[i].apply(null, listenerArgs);
1247-
} catch (e) {
1248-
$exceptionHandler(e);
1249-
}
1250-
}
1234+
invokeListeners(scope, event, listenerArgs, name);
1235+
12511236
//if any listener on the current scope stops propagation, prevent bubbling
12521237
if (stopPropagation) {
12531238
event.currentScope = null;
@@ -1299,28 +1284,11 @@ function $RootScopeProvider() {
12991284

13001285
if (!target.$$listenerCount[name]) return event;
13011286

1302-
var listenerArgs = concat([event], arguments, 1),
1303-
listeners, i, length;
1287+
var listenerArgs = concat([event], arguments, 1);
13041288

13051289
//down while you can, then up and next sibling or up and next sibling until back at root
13061290
while ((current = next)) {
1307-
event.currentScope = current;
1308-
listeners = current.$$listeners[name] || [];
1309-
for (i = 0, length = listeners.length; i < length; i++) {
1310-
// if listeners were deregistered, defragment the array
1311-
if (!listeners[i]) {
1312-
listeners.splice(i, 1);
1313-
i--;
1314-
length--;
1315-
continue;
1316-
}
1317-
1318-
try {
1319-
listeners[i].apply(null, listenerArgs);
1320-
} catch (e) {
1321-
$exceptionHandler(e);
1322-
}
1323-
}
1291+
invokeListeners(current, event, listenerArgs, name);
13241292

13251293
// Insanity Warning: scope depth-first traversal
13261294
// yes, this code is a bit crazy, but it works and we have tests to prove it!
@@ -1350,6 +1318,27 @@ function $RootScopeProvider() {
13501318

13511319
return $rootScope;
13521320

1321+
function invokeListeners(scope, event, listenerArgs, name) {
1322+
var listeners = scope.$$listeners[name];
1323+
if (listeners) {
1324+
if (listeners.$$index !== undefined) {
1325+
throw $rootScopeMinErr('inevt', '{0} already $emit/$broadcast-ing on scope ({1})', name, scope.$id);
1326+
}
1327+
event.currentScope = scope;
1328+
try {
1329+
for (listeners.$$index = 0; listeners.$$index < listeners.length; listeners.$$index++) {
1330+
try {
1331+
//allow all listeners attached to the current scope to run
1332+
listeners[listeners.$$index].apply(null, listenerArgs);
1333+
} catch (e) {
1334+
$exceptionHandler(e);
1335+
}
1336+
}
1337+
} finally {
1338+
listeners.$$index = undefined;
1339+
}
1340+
}
1341+
}
13531342

13541343
function beginPhase(phase) {
13551344
if ($rootScope.$$phase) {

test/ng/rootScopeSpec.js

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2316,6 +2316,19 @@ describe('Scope', function() {
23162316
}));
23172317

23182318

2319+
// See issue https://github.com/angular/angular.js/issues/16135
2320+
it('should deallocate the listener array entry', inject(function($rootScope) {
2321+
var remove1 = $rootScope.$on('abc', noop);
2322+
$rootScope.$on('abc', noop);
2323+
2324+
expect($rootScope.$$listeners['abc'].length).toBe(2);
2325+
2326+
remove1();
2327+
2328+
expect($rootScope.$$listeners['abc'].length).toBe(1);
2329+
}));
2330+
2331+
23192332
it('should call next listener after removing the current listener via its own handler', inject(function($rootScope) {
23202333
var listener1 = jasmine.createSpy('listener1').and.callFake(function() { remove1(); });
23212334
var remove1 = $rootScope.$on('abc', listener1);
@@ -2448,6 +2461,107 @@ describe('Scope', function() {
24482461
expect($rootScope.$$listenerCount).toEqual({abc: 1});
24492462
expect(child.$$listenerCount).toEqual({abc: 1});
24502463
}));
2464+
2465+
2466+
it('should throw on recursive $broadcast', inject(function($rootScope) {
2467+
$rootScope.$on('e', function() { $rootScope.$broadcast('e'); });
2468+
2469+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2470+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2471+
}));
2472+
2473+
2474+
it('should throw on nested recursive $broadcast', inject(function($rootScope) {
2475+
$rootScope.$on('e2', function() { $rootScope.$broadcast('e'); });
2476+
$rootScope.$on('e', function() { $rootScope.$broadcast('e2'); });
2477+
2478+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2479+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2480+
}));
2481+
2482+
2483+
it('should throw on recursive $emit', inject(function($rootScope) {
2484+
$rootScope.$on('e', function() { $rootScope.$emit('e'); });
2485+
2486+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2487+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2488+
}));
2489+
2490+
2491+
it('should throw on nested recursive $emit', inject(function($rootScope) {
2492+
$rootScope.$on('e2', function() { $rootScope.$emit('e'); });
2493+
$rootScope.$on('e', function() { $rootScope.$emit('e2'); });
2494+
2495+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2496+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2497+
}));
2498+
2499+
2500+
it('should throw on recursive $broadcast on child listener', inject(function($rootScope) {
2501+
var child = $rootScope.$new();
2502+
child.$on('e', function() { $rootScope.$broadcast('e'); });
2503+
2504+
expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)');
2505+
expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)');
2506+
}));
2507+
2508+
2509+
it('should throw on nested recursive $broadcast on child listener', inject(function($rootScope) {
2510+
var child = $rootScope.$new();
2511+
child.$on('e2', function() { $rootScope.$broadcast('e'); });
2512+
child.$on('e', function() { $rootScope.$broadcast('e2'); });
2513+
2514+
expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)');
2515+
expect(function() { child.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (2)');
2516+
}));
2517+
2518+
2519+
it('should throw on recursive $emit parent listener', inject(function($rootScope) {
2520+
var child = $rootScope.$new();
2521+
$rootScope.$on('e', function() { child.$emit('e'); });
2522+
2523+
expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2524+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2525+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2526+
}));
2527+
2528+
2529+
it('should throw on nested recursive $emit parent listener', inject(function($rootScope) {
2530+
var child = $rootScope.$new();
2531+
$rootScope.$on('e2', function() { child.$emit('e'); });
2532+
$rootScope.$on('e', function() { child.$emit('e2'); });
2533+
2534+
expect(function() { child.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2535+
expect(function() { $rootScope.$emit('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2536+
expect(function() { $rootScope.$broadcast('e', 5); }).toThrowMinErr('$rootScope', 'inevt', 'e already $emit/$broadcast-ing on scope (1)');
2537+
}));
2538+
2539+
2540+
it('should clear recursive state of $broadcast if $exceptionHandler rethrows', function() {
2541+
module(function($exceptionHandlerProvider) {
2542+
$exceptionHandlerProvider.mode('rethrow');
2543+
});
2544+
inject(function($rootScope) {
2545+
var throwingListener = jasmine.createSpy('thrower').and.callFake(function() {
2546+
throw new Error('Listener Error!');
2547+
});
2548+
var secondListener = jasmine.createSpy('second');
2549+
2550+
$rootScope.$on('e', throwingListener);
2551+
$rootScope.$on('e', secondListener);
2552+
2553+
expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!');
2554+
expect(throwingListener).toHaveBeenCalled();
2555+
expect(secondListener).not.toHaveBeenCalled();
2556+
2557+
throwingListener.calls.reset();
2558+
secondListener.calls.reset();
2559+
2560+
expect(function() { $rootScope.$broadcast('e'); }).toThrowError('Listener Error!');
2561+
expect(throwingListener).toHaveBeenCalled();
2562+
expect(secondListener).not.toHaveBeenCalled();
2563+
});
2564+
});
24512565
});
24522566
});
24532567

@@ -2537,7 +2651,7 @@ describe('Scope', function() {
25372651
expect(spy1).toHaveBeenCalledOnce();
25382652
expect(spy2).toHaveBeenCalledOnce();
25392653
expect(spy3).toHaveBeenCalledOnce();
2540-
expect(child.$$listeners['evt'].length).toBe(3); // cleanup will happen on next $emit
2654+
expect(child.$$listeners['evt'].length).toBe(2);
25412655

25422656
spy1.calls.reset();
25432657
spy2.calls.reset();
@@ -2571,7 +2685,7 @@ describe('Scope', function() {
25712685
expect(spy1).toHaveBeenCalledOnce();
25722686
expect(spy2).toHaveBeenCalledOnce();
25732687
expect(spy3).toHaveBeenCalledOnce();
2574-
expect(child.$$listeners['evt'].length).toBe(3); //cleanup will happen on next $broadcast
2688+
expect(child.$$listeners['evt'].length).toBe(2);
25752689

25762690
spy1.calls.reset();
25772691
spy2.calls.reset();

0 commit comments

Comments
 (0)