Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 2143d9c

Browse files
JiaLiPassionmhevery
authored andcommitted
fix(event): fix memory leak for once, add more test cases (#841)
* fix(event): fix memory leak for once, add more test cases * add comments
1 parent f301fa2 commit 2143d9c

File tree

8 files changed

+843
-80
lines changed

8 files changed

+843
-80
lines changed

Diff for: lib/browser/browser.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate
4343
// patch XMLHttpRequestEventTarget's addEventListener/removeEventListener
4444
const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget'];
4545
if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) {
46-
// TODO: @JiaLiPassion, add this back later.
47-
api.patchEventTargetMethods(XMLHttpRequestEventTarget.prototype);
46+
api.patchEventTarget(global, [XMLHttpRequestEventTarget.prototype]);
4847
}
4948
patchClass('MutationObserver');
5049
patchClass('WebKitMutationObserver');

Diff for: lib/browser/event-target.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export function eventTargetPatch(_global: any, api: _ZonePrivate) {
101101
const type = _global[apis[i]];
102102
apiTypes.push(type && type.prototype);
103103
}
104-
patchEventTarget(api, _global, apiTypes, {validateHandler: checkIEAndCrossContext});
104+
patchEventTarget(_global, apiTypes, {validateHandler: checkIEAndCrossContext});
105+
api.patchEventTarget = patchEventTarget;
105106

106107
return true;
107108
}

Diff for: lib/browser/shadydom.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Zone.__load_patch('shadydom', (global: any, Zone: ZoneType, api: _ZonePrivate) =
1414
if (windowPrototype && windowPrototype.hasOwnProperty('addEventListener')) {
1515
(windowPrototype as any)[Zone.__symbol__('addEventListener')] = null;
1616
(windowPrototype as any)[Zone.__symbol__('removeEventListener')] = null;
17-
api.patchEventTargetMethods(windowPrototype);
17+
api.patchEventTarget(global, [windowPrototype]);
1818
}
1919
if (Node.prototype.hasOwnProperty('addEventListener')) {
2020
(Node.prototype as any)[Zone.__symbol__('addEventListener')] = null;
2121
(Node.prototype as any)[Zone.__symbol__('removeEventListener')] = null;
22-
api.patchEventTargetMethods(Node.prototype);
22+
api.patchEventTarget(global, [Node.prototype]);
2323
}
2424
});

Diff for: lib/browser/webapis-media-query.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Zone.__load_patch('mediaQuery', (global: any, Zone: ZoneType, api: _ZonePrivate)
99
if (!global['MediaQueryList']) {
1010
return;
1111
}
12-
api.patchEventTargetMethods(
13-
global['MediaQueryList'].prototype,
12+
api.patchEventTarget(
13+
global, [global['MediaQueryList'].prototype],
1414
{addEventListenerFnName: 'addListener', removeEventListenerFnName: 'removeListener'});
1515
});

Diff for: lib/common/events.ts

+73-19
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,24 @@ export interface PatchEventTargetOptions {
4343
}
4444

4545
export function patchEventTarget(
46-
api: _ZonePrivate, _global: any, apis: any[], patchOptions?: PatchEventTargetOptions) {
46+
_global: any, apis: any[], patchOptions?: PatchEventTargetOptions) {
47+
const ADD_EVENT_LISTENER =
48+
(patchOptions && patchOptions.addEventListenerFnName) || 'addEventListener';
49+
const REMOVE_EVENT_LISTENER =
50+
(patchOptions && patchOptions.removeEventListenerFnName) || 'removeEventListener';
51+
52+
const LISTENERS_EVENT_LISTENER =
53+
(patchOptions && patchOptions.listenersFnName) || 'eventListeners';
54+
const REMOVE_ALL_LISTENERS_EVENT_LISTENER =
55+
(patchOptions && patchOptions.removeAllFnName) || 'removeAllListeners';
56+
57+
const zoneSymbolAddEventListener = zoneSymbol(ADD_EVENT_LISTENER);
58+
59+
const ADD_EVENT_LISTENER_SOURCE = '.' + ADD_EVENT_LISTENER + ':';
60+
61+
const PREPEND_EVENT_LISTENER = 'prependListener';
62+
const PREPEND_EVENT_LISTENER_SOURCE = '.' + PREPEND_EVENT_LISTENER + ':';
63+
4764
const invokeTask = function(task: any, target: any, event: Event) {
4865
// for better performance, check isRemoved which is set
4966
// by removeEventListener
@@ -52,12 +69,20 @@ export function patchEventTarget(
5269
}
5370
const delegate = task.callback;
5471
if (typeof delegate === OBJECT_TYPE && delegate.handleEvent) {
55-
// create the bind version of handleEvnet when invoke
72+
// create the bind version of handleEvent when invoke
5673
task.callback = (event: Event) => delegate.handleEvent(event);
5774
task.originalDelegate = delegate;
5875
}
5976
// invoke static task.invoke
6077
task.invoke(task, target, [event]);
78+
const options = task.options;
79+
if (options && typeof options === 'object' && options.once) {
80+
// if options.once is true, after invoke once remove listener here
81+
// only browser need to do this, nodejs eventEmitter will cal removeListener
82+
// inside EventEmitter.once
83+
const delegate = task.originalDelegate ? task.originalDelegate : task.callback;
84+
target[REMOVE_EVENT_LISTENER].apply(target, [event.type, delegate, options]);
85+
}
6186
};
6287

6388
// global shared zoneAwareCallback to handle all event callback with capture = false
@@ -67,6 +92,7 @@ export function patchEventTarget(
6792
const tasks = target[zoneSymbolEventNames[event.type][FALSE_STR]];
6893
if (tasks) {
6994
// invoke all tasks which attached to current target with given event.type and capture = false
95+
// for performance concern, if task.length === 1, just invoke
7096
if (tasks.length === 1) {
7197
invokeTask(tasks[0], target, event);
7298
} else {
@@ -88,6 +114,7 @@ export function patchEventTarget(
88114
const tasks = target[zoneSymbolEventNames[event.type][TRUE_STR]];
89115
if (tasks) {
90116
// invoke all tasks which attached to current target with given event.type and capture = false
117+
// for performance concern, if task.length === 1, just invoke
91118
if (tasks.length === 1) {
92119
invokeTask(tasks[0], target, event);
93120
} else {
@@ -106,22 +133,6 @@ export function patchEventTarget(
106133
if (!obj) {
107134
return false;
108135
}
109-
const ADD_EVENT_LISTENER =
110-
(patchOptions && patchOptions.addEventListenerFnName) || 'addEventListener';
111-
const REMOVE_EVENT_LISTENER =
112-
(patchOptions && patchOptions.removeEventListenerFnName) || 'removeEventListener';
113-
114-
const LISTENERS_EVENT_LISTENER =
115-
(patchOptions && patchOptions.listenersFnName) || 'eventListeners';
116-
const REMOVE_ALL_LISTENERS_EVENT_LISTENER =
117-
(patchOptions && patchOptions.removeAllFnName) || 'removeAllListeners';
118-
119-
const zoneSymbolAddEventListener = zoneSymbol(ADD_EVENT_LISTENER);
120-
121-
const ADD_EVENT_LISTENER_SOURCE = '.' + ADD_EVENT_LISTENER + ':';
122-
123-
const PREPEND_EVENT_LISTENER = 'prependListener';
124-
const PREPEND_EVENT_LISTENER_SOURCE = '.' + PREPEND_EVENT_LISTENER + ':';
125136

126137
let useGlobalCallback = true;
127138
if (patchOptions && patchOptions.useGlobalCallback !== undefined) {
@@ -188,6 +199,34 @@ export function patchEventTarget(
188199
};
189200

190201
const customCancelGlobal = function(task: any) {
202+
// if task is not marked as isRemoved, this call is directly
203+
// from Zone.prototype.cancelTask, we should remove the task
204+
// from tasksList of target first
205+
if (!task.isRemoved) {
206+
const symbolEventNames = zoneSymbolEventNames[task.eventName];
207+
let symbolEventName;
208+
if (symbolEventNames) {
209+
symbolEventName = symbolEventNames[task.capture ? TRUE_STR : FALSE_STR];
210+
}
211+
const existingTasks = symbolEventName && task.target[symbolEventName];
212+
if (existingTasks) {
213+
for (let i = 0; i < existingTasks.length; i++) {
214+
const existingTask = existingTasks[i];
215+
if (existingTask === task) {
216+
existingTasks.splice(i, 1);
217+
// set isRemoved to data for faster invokeTask check
218+
task.isRemoved = true;
219+
if (existingTasks.length === 0) {
220+
// all tasks for the eventName + capture have gone,
221+
// remove globalZoneAwareCallback and remove the task cache from target
222+
task.allRemoved = true;
223+
task.target[symbolEventName] = null;
224+
}
225+
break;
226+
}
227+
}
228+
}
229+
}
191230
// if all tasks for the eventName + capture have gone,
192231
// we will really remove the global event callback,
193232
// if not, return
@@ -262,6 +301,7 @@ export function patchEventTarget(
262301
const options = arguments[2];
263302

264303
let capture;
304+
let once = false;
265305
if (options === undefined) {
266306
capture = false;
267307
} else if (options === true) {
@@ -270,6 +310,7 @@ export function patchEventTarget(
270310
capture = false;
271311
} else {
272312
capture = options ? !!options.capture : false;
313+
once = options ? !!options.once : false;
273314
}
274315

275316
const zone = Zone.current;
@@ -316,6 +357,12 @@ export function patchEventTarget(
316357
// do not create a new object as task.data to pass those things
317358
// just use the global shared one
318359
taskData.options = options;
360+
if (once) {
361+
// if addEventListener with once options, we don't pass it to
362+
// native addEventListener, instead we keep the once setting
363+
// and handle ourselves.
364+
taskData.options.once = false;
365+
}
319366
taskData.target = target;
320367
taskData.capture = capture;
321368
taskData.eventName = eventName;
@@ -327,6 +374,9 @@ export function patchEventTarget(
327374

328375
// have to save those information to task in case
329376
// application may call task.zone.cancelTask() directly
377+
if (once) {
378+
options.once = true;
379+
}
330380
task.options = options;
331381
task.target = target;
332382
task.capture = capture;
@@ -434,10 +484,15 @@ export function patchEventTarget(
434484
const prop = keys[i];
435485
const match = EVENT_NAME_SYMBOL_REGX.exec(prop);
436486
let evtName = match && match[1];
487+
// in nodejs EventEmitter, removeListener event is
488+
// used for monitoring the removeListener call,
489+
// so just keep removeListener eventListener until
490+
// all other eventListeners are removed
437491
if (evtName && evtName !== 'removeListener') {
438492
this[REMOVE_ALL_LISTENERS_EVENT_LISTENER].apply(this, [evtName]);
439493
}
440494
}
495+
// remove removeListener listener finally
441496
this[REMOVE_ALL_LISTENERS_EVENT_LISTENER].apply(this, ['removeListener']);
442497
} else {
443498
const symbolEventNames = zoneSymbolEventNames[eventName];
@@ -486,7 +541,6 @@ export function patchEventTarget(
486541
results[i] = patchEventTargetMethods(apis[i], patchOptions);
487542
}
488543

489-
api.patchEventTargetMethods = patchEventTargetMethods;
490544
return results;
491545
}
492546

Diff for: lib/node/events.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Zone.__load_patch('EventEmitter', (global: any, Zone: ZoneType, api: _ZonePrivat
3333
};
3434

3535
function patchEventEmitterMethods(obj: any) {
36-
const result = patchEventTarget(api, global, [obj], {
36+
const result = patchEventTarget(global, [obj], {
3737
useGlobalCallback: false,
3838
addEventListenerFnName: EE_ADD_LISTENER,
3939
removeEventListenerFnName: EE_REMOVE_LISTENER,

Diff for: lib/zone.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ interface _ZonePrivate {
320320
onUnhandledError: (error: Error) => void;
321321
microtaskDrainDone: () => void;
322322
showUncaughtError: () => boolean;
323-
patchEventTargetMethods: (obj: any, options?: any) => boolean;
323+
patchEventTarget: (global: any, apis: any[], options?: any) => boolean[];
324324
patchOnProperties: (obj: any, properties: string[]) => void;
325325
patchMethod:
326326
(target: any, name: string,
@@ -1307,7 +1307,7 @@ const Zone: ZoneType = (function(global: any) {
13071307
microtaskDrainDone: noop,
13081308
scheduleMicroTask: scheduleMicroTask,
13091309
showUncaughtError: () => !(Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')],
1310-
patchEventTargetMethods: () => false,
1310+
patchEventTarget: () => [],
13111311
patchOnProperties: noop,
13121312
patchMethod: () => noop,
13131313
};

0 commit comments

Comments
 (0)