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

Commit b3a76d3

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(eventListener): fix #798, improve EventTarget.addEventListener performance (#812)
* feat(performance): fix #798, improve EventTarget.addEventListener performance * add test cases * add comment to explain the implementations * add test cases for resschedule eventTask * add IE/edge and cross context check * modify document for extra flags * minor changes, define some const, move logic into __load_patch * let nodejs use new addListener/removeListener method * add nodejs test example * add eventListeners/removeAllListeners test cases for browser
1 parent 98f3903 commit b3a76d3

18 files changed

+1219
-593
lines changed

MODULE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Below is the full list of current support modules.
3636
|timers|setTimeout/setInterval/setImmediate will be patched as Zone MacroTask|__Zone_disable_timer = true|
3737
|blocking|alert/prompt/confirm will be patched as Zone.run|__Zone_disable_blocking = true|
3838
|EventTarget|target.addEventListener will be patched as Zone aware EventTask|__Zone_disable_EventTarget = true|
39+
|IE BrowserTools check|in IE, browser tool will not use zone patched eventListener|__Zone_disable_IE_check = true|
40+
|CrossContext check|in webdriver, enable check event listener is cross context|__Zone_enable_cross_context_check = true|
3941
|XHR|XMLHttpRequest will be patched as Zone aware MacroTask|__Zone_disable_XHR = true|
4042
|geolocation|navigator.geolocation's prototype will be patched as Zone.run|__Zone_disable_geolocation = true|
4143
|PromiseRejectionEvent|PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error|__Zone_disable_PromiseRejectionEvent = true|

example/benchmarks/addEventListener.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="utf-8">
55
<title>Zone.js addEventListenerBenchmark</title>
6-
<link rel="stylesheet" href="css/style.css">
6+
<link rel="stylesheet" href="../css/style.css">
77
<script src="../../dist/zone.js"></script>
88
</head>
99
<body>

example/benchmarks/event_emitter.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
const events = require('events');
9+
const EventEmitter = events.EventEmitter;
10+
require('../../dist/zone-node');
11+
12+
const emitters = [];
13+
const callbacks = [];
14+
const size = 100000;
15+
for (let i = 0; i < size; i++) {
16+
const emitter = new EventEmitter();
17+
const callback = (function (i) { return function () { console.log(i); }; })(i);
18+
emitters[i] = emitter;
19+
callbacks[i] = callback;
20+
}
21+
22+
function addRemoveCallback(reuse, useZone) {
23+
const start = new Date();
24+
let callback = callbacks[0];
25+
for (let i = 0; i < size; i++) {
26+
const emitter = emitters[i];
27+
if (!reuse) callback = callbacks[i];
28+
if (useZone)
29+
emitter.on('msg', callback);
30+
else
31+
emitter.__zone_symbol__addListener('msg', callback);
32+
}
33+
34+
for (let i = 0; i < size; i++) {
35+
const emitter = emitters[i];
36+
if (!reuse) callback = callbacks[i];
37+
if (useZone)
38+
emitter.removeListener('msg', callback);
39+
else
40+
emitter.__zone_symbol__removeListener('msg', callback);
41+
}
42+
const end = new Date();
43+
console.log(useZone ? 'use zone' : 'native', reuse ? 'reuse' : 'new');
44+
console.log("Execution time: %dms", end - start);
45+
}
46+
47+
addRemoveCallback(false, false);
48+
addRemoveCallback(false, true);
49+
addRemoveCallback(true, false);
50+
addRemoveCallback(true, true);

lib/browser/browser.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {findEventTasks} from '../common/events';
910
import {patchTimer} from '../common/timers';
10-
import {findEventTask, patchClass, patchEventTargetMethods, patchMacroTask, patchMethod, patchOnProperties, patchPrototype, zoneSymbol} from '../common/utils';
11+
import {patchClass, patchMacroTask, patchMethod, patchOnProperties, patchPrototype, zoneSymbol} from '../common/utils';
1112

1213
import {propertyPatch} from './define-property';
1314
import {eventTargetPatch} from './event-target';
@@ -38,11 +39,12 @@ Zone.__load_patch('blocking', (global: any, Zone: ZoneType, api: _ZonePrivate) =
3839
});
3940

4041
Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
41-
eventTargetPatch(global);
42+
eventTargetPatch(global, api);
4243
// patch XMLHttpRequestEventTarget's addEventListener/removeEventListener
4344
const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget'];
4445
if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) {
45-
patchEventTargetMethods(XMLHttpRequestEventTarget.prototype);
46+
// TODO: @JiaLiPassion, add this back later.
47+
api.patchEventTargetMethods(XMLHttpRequestEventTarget.prototype);
4648
}
4749
patchClass('MutationObserver');
4850
patchClass('WebKitMutationObserver');
@@ -180,7 +182,7 @@ Zone.__load_patch('PromiseRejectionEvent', (global: any, Zone: ZoneType, api: _Z
180182
// handle unhandled promise rejection
181183
function findPromiseRejectionHandler(evtName: string) {
182184
return function(e: any) {
183-
const eventTasks = findEventTask(global, evtName);
185+
const eventTasks = findEventTasks(global, evtName);
184186
eventTasks.forEach(eventTask => {
185187
// windows has added unhandledrejection event listener
186188
// trigger the event listener
@@ -204,7 +206,6 @@ Zone.__load_patch('PromiseRejectionEvent', (global: any, Zone: ZoneType, api: _Z
204206

205207

206208
Zone.__load_patch('util', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
207-
api.patchEventTargetMethods = patchEventTargetMethods;
208209
api.patchOnProperties = patchOnProperties;
209210
api.patchMethod = patchMethod;
210211
});

lib/browser/event-target.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {patchEventTargetMethods} from '../common/utils';
9+
import {FALSE_STR, globalSources, patchEventTargetMethods, TRUE_STR, ZONE_SYMBOL_PREFIX, zoneSymbolEventNames} from '../common/events';
10+
import {attachOriginToPatched, isIEOrEdge, zoneSymbol} from '../common/utils';
1011

11-
const WTF_ISSUE_555 =
12-
'Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video';
13-
const NO_EVENT_TARGET =
14-
'ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket'
15-
.split(',');
16-
const EVENT_TARGET = 'EventTarget';
12+
import {eventNames} from './property-descriptor';
13+
14+
export function eventTargetPatch(_global: any, api: _ZonePrivate) {
15+
const WTF_ISSUE_555 =
16+
'Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video';
17+
const NO_EVENT_TARGET =
18+
'ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket'
19+
.split(',');
20+
const EVENT_TARGET = 'EventTarget';
1721

18-
export function eventTargetPatch(_global: any) {
1922
let apis = [];
2023
const isWtf = _global['wtf'];
24+
const WTF_ISSUE_555_ARRAY = WTF_ISSUE_555.split(',');
25+
2126
if (isWtf) {
2227
// Workaround for: https://github.com/google/tracing-framework/issues/555
23-
apis = WTF_ISSUE_555.split(',').map((v) => 'HTML' + v + 'Element').concat(NO_EVENT_TARGET);
28+
apis = WTF_ISSUE_555_ARRAY.map((v) => 'HTML' + v + 'Element').concat(NO_EVENT_TARGET);
2429
} else if (_global[EVENT_TARGET]) {
2530
apis.push(EVENT_TARGET);
2631
} else {
@@ -29,8 +34,73 @@ export function eventTargetPatch(_global: any) {
2934
apis = NO_EVENT_TARGET;
3035
}
3136

37+
const isDisableIECheck = _global['__Zone_disable_IE_check'] || false;
38+
const isEnableCrossContextCheck = _global['__Zone_enable_cross_context_check'] || false;
39+
const ieOrEdge = isIEOrEdge();
40+
41+
const ADD_EVENT_LISTENER_SOURCE = '.addEventListener:';
42+
const FUNCTION_WRAPPER = '[object FunctionWrapper]';
43+
const BROWSER_TOOLS = 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }';
44+
45+
// predefine all __zone_symbol__ + eventName + true/false string
46+
for (let i = 0; i < eventNames.length; i++) {
47+
const eventName = eventNames[i];
48+
const falseEventName = eventName + FALSE_STR;
49+
const trueEventName = eventName + TRUE_STR;
50+
const symbol = ZONE_SYMBOL_PREFIX + falseEventName;
51+
const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName;
52+
zoneSymbolEventNames[eventName] = {};
53+
zoneSymbolEventNames[eventName][FALSE_STR] = symbol;
54+
zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture;
55+
}
56+
57+
// predefine all task.source string
58+
for (let i = 0; i < WTF_ISSUE_555.length; i++) {
59+
const target: any = WTF_ISSUE_555_ARRAY[i];
60+
const targets: any = globalSources[target] = {};
61+
for (let j = 0; j < eventNames.length; j++) {
62+
const eventName = eventNames[j];
63+
targets[eventName] = target + ADD_EVENT_LISTENER_SOURCE + eventName;
64+
}
65+
}
66+
67+
const checkIEAndCrossContext = function(
68+
nativeDelegate: any, delegate: any, target: any, args: any) {
69+
if (!isDisableIECheck && ieOrEdge) {
70+
if (isEnableCrossContextCheck) {
71+
try {
72+
const testString = delegate.toString();
73+
if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
74+
nativeDelegate.apply(target, args);
75+
return false;
76+
}
77+
} catch (error) {
78+
nativeDelegate.apply(target, args);
79+
return false;
80+
}
81+
} else {
82+
const testString = delegate.toString();
83+
if ((testString === FUNCTION_WRAPPER || testString == BROWSER_TOOLS)) {
84+
nativeDelegate.apply(target, args);
85+
return false;
86+
}
87+
}
88+
} else if (isEnableCrossContextCheck) {
89+
try {
90+
delegate.toString();
91+
} catch (error) {
92+
nativeDelegate.apply(target, args);
93+
return false;
94+
}
95+
}
96+
return true;
97+
};
98+
3299
for (let i = 0; i < apis.length; i++) {
33100
const type = _global[apis[i]];
34-
patchEventTargetMethods(type && type.prototype);
101+
patchEventTargetMethods(type && type.prototype, {validateHandler: checkIEAndCrossContext});
35102
}
103+
104+
api.patchEventTargetMethods = patchEventTargetMethods;
105+
return true;
36106
}

lib/browser/property-descriptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ const IDBIndexEventNames =
222222
['upgradeneeded', 'complete', 'abort', 'success', 'error', 'blocked', 'versionchange', 'close'];
223223
const websocketEventNames = ['close', 'error', 'open', 'message'];
224224

225-
const eventNames = globalEventHandlersEventNames.concat(
225+
export const eventNames = globalEventHandlersEventNames.concat(
226226
webglEventNames, formEventNames, detailEventNames, documentEventNames, windowEventNames,
227227
htmlElementEventNames, ieElementEventNames);
228228

lib/browser/webapis-media-query.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,6 @@ Zone.__load_patch('mediaQuery', (global: any, Zone: ZoneType, api: _ZonePrivate)
1010
return;
1111
}
1212
api.patchEventTargetMethods(
13-
global['MediaQueryList'].prototype, 'addListener', 'removeListener',
14-
(self: any, args: any[]) => {
15-
return {
16-
useCapturing: false,
17-
eventName: 'mediaQuery',
18-
handler: args[0],
19-
target: self || global,
20-
name: 'mediaQuery',
21-
invokeAddFunc: function(addFnSymbol: any, delegate: any) {
22-
if (delegate && (<Task>delegate).invoke) {
23-
return this.target[addFnSymbol]((<Task>delegate).invoke);
24-
} else {
25-
return this.target[addFnSymbol](delegate);
26-
}
27-
},
28-
invokeRemoveFunc: function(removeFnSymbol: any, delegate: any) {
29-
if (delegate && (<Task>delegate).invoke) {
30-
return this.target[removeFnSymbol]((<Task>delegate).invoke);
31-
} else {
32-
return this.target[removeFnSymbol](delegate);
33-
}
34-
}
35-
};
36-
});
13+
global['MediaQueryList'].prototype,
14+
{addEventListenerFnName: 'addListener', removeEventListenerFnName: 'removeListener'});
3715
});

lib/browser/websocket.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {patchEventTargetMethods, patchOnProperties} from '../common/utils';
9+
import {patchEventTargetMethods} from '../common/events';
10+
import {patchOnProperties} from '../common/utils';
1011

1112
// we have to patch the instance since the proto is non-configurable
1213
export function apply(_global: any) {

0 commit comments

Comments
 (0)