Skip to content

Commit 68247ee

Browse files
committed
fix($rootScope): fix potential memory leak when removing scope listeners
Previously the array entry for listeners was set to null but the array size was not trimmed until the event was broadcasted again (see angular@e6966e0). By keeping track of the listener iteration index globally it can be adjusted if a listener removal effects the index. Fixes angular#16135
1 parent 8c322f0 commit 68247ee

File tree

2 files changed

+26
-28
lines changed

2 files changed

+26
-28
lines changed

src/ng/rootScope.js

+11-26
Original file line numberDiff line numberDiff line change
@@ -1181,10 +1181,12 @@ 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+
if (index <= namedListeners.$$listenerIndex) {
1188+
namedListeners.$$listenerIndex--;
1189+
}
11881190
}
11891191
};
11901192
},
@@ -1226,24 +1228,15 @@ 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 {
12331234
namedListeners = scope.$$listeners[name] || empty;
12341235
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-
}
1236+
for (namedListeners.$$listenerIndex = 0; namedListeners.$$listenerIndex < namedListeners.length; namedListeners.$$listenerIndex++) {
12441237
try {
12451238
//allow all listeners attached to the current scope to run
1246-
namedListeners[i].apply(null, listenerArgs);
1239+
namedListeners[namedListeners.$$listenerIndex].apply(null, listenerArgs);
12471240
} catch (e) {
12481241
$exceptionHandler(e);
12491242
}
@@ -1300,23 +1293,15 @@ function $RootScopeProvider() {
13001293
if (!target.$$listenerCount[name]) return event;
13011294

13021295
var listenerArgs = concat([event], arguments, 1),
1303-
listeners, i, length;
1296+
listeners;
13041297

13051298
//down while you can, then up and next sibling or up and next sibling until back at root
13061299
while ((current = next)) {
13071300
event.currentScope = current;
13081301
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-
1302+
for (listeners.$$listenerIndex = 0; listeners.$$listenerIndex < listeners.length; listeners.$$listenerIndex++) {
13181303
try {
1319-
listeners[i].apply(null, listenerArgs);
1304+
listeners[listeners.$$listenerIndex].apply(null, listenerArgs);
13201305
} catch (e) {
13211306
$exceptionHandler(e);
13221307
}

test/ng/rootScopeSpec.js

+15-2
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 when removing current', inject(function($rootScope) {
23202333
var listener1 = jasmine.createSpy().and.callFake(function() { remove1(); });
23212334
var remove1 = $rootScope.$on('abc', listener1);
@@ -2531,7 +2544,7 @@ describe('Scope', function() {
25312544
expect(spy1).toHaveBeenCalledOnce();
25322545
expect(spy2).toHaveBeenCalledOnce();
25332546
expect(spy3).toHaveBeenCalledOnce();
2534-
expect(child.$$listeners['evt'].length).toBe(3); // cleanup will happen on next $emit
2547+
expect(child.$$listeners['evt'].length).toBe(2);
25352548

25362549
spy1.calls.reset();
25372550
spy2.calls.reset();
@@ -2565,7 +2578,7 @@ describe('Scope', function() {
25652578
expect(spy1).toHaveBeenCalledOnce();
25662579
expect(spy2).toHaveBeenCalledOnce();
25672580
expect(spy3).toHaveBeenCalledOnce();
2568-
expect(child.$$listeners['evt'].length).toBe(3); //cleanup will happen on next $broadcast
2581+
expect(child.$$listeners['evt'].length).toBe(2);
25692582

25702583
spy1.calls.reset();
25712584
spy2.calls.reset();

0 commit comments

Comments
 (0)