From 0f298a13dbd141e5440d1388b124d03384641efe Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Tue, 23 Feb 2021 21:45:03 +0800 Subject: [PATCH] refactor(zone.js): remove onProp eventNames array to reduce the bundle size (#40962) Zone.js supports the google closure compiler in the advance optimization mode, to prevent closure compiler modify the `onProperty` name such as `Element.prototype.onclick`, Zone.js implements the `onProperty` patch logic by declaring all the event names in the source code, this increases the bundle size and also require updating the event names array to keep the information updated. Now google closure compiler has the required event names defined in the built-in externs files, so zone.js can use more simple implementation and decrease the bundle size of zone.js (about 4k). PR Close #40962 --- goldens/size-tracking/aio-payloads.json | 2 +- .../size-tracking/integration-payloads.json | 48 +-- packages/zone.js/lib/browser/api-util.ts | 5 +- .../lib/browser/property-descriptor.ts | 292 ++----------- packages/zone.js/lib/common/utils.ts | 20 +- packages/zone.js/test/assets/empty-worker.js | 7 + packages/zone.js/test/browser/browser.spec.ts | 408 +++++++++++++++++- 7 files changed, 460 insertions(+), 322 deletions(-) create mode 100644 packages/zone.js/test/assets/empty-worker.js diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 53a33f37e35..36d844487ae 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -16,7 +16,7 @@ "uncompressed": { "runtime": 4343, "main": 450179, - "polyfills": 37823, + "polyfills": 33349, "styles": 70416, "light-theme": 77582, "dark-theme": 77711 diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index c950f6df56f..80c689f523b 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -4,7 +4,7 @@ "uncompressed": { "runtime": 1083, "main": 126218, - "polyfills": 37778 + "polyfills": 33304 } } }, @@ -16,7 +16,7 @@ "main": "Likely there is a missing PURE annotation https://github.com/angular/angular/pull/43344", "main": "Tracking issue: https://github.com/angular/angular/issues/43568", "main": 20378, - "polyfills": 37802 + "polyfills": 33328 } } }, @@ -25,7 +25,7 @@ "uncompressed": { "runtime": 1105, "main": 131882, - "polyfills": 37800 + "polyfills": 33326 } } }, @@ -34,7 +34,7 @@ "uncompressed": { "runtime": 929, "main": 124544, - "polyfills": 38488 + "polyfills": 34010 } } }, @@ -43,7 +43,7 @@ "uncompressed": { "runtime": 2835, "main": 233348, - "polyfills": 37796, + "polyfills": 33322, "src_app_lazy_lazy_module_ts": 795 } } @@ -53,27 +53,27 @@ "uncompressed": { "runtime": 1063, "main": 158556, - "polyfills": 37758 + "polyfills": 33284 } - } - }, - "animations": { - "master": { - "uncompressed": { - "runtime": 1070, - "main": 158300, - "polyfills": 37768 + }, + "animations": { + "master": { + "uncompressed": { + "runtime": 1070, + "main": 158300, + "polyfills": 33294 + } } - } - }, - "hello_world__closure": { - "master": { - "uncompressed": { - "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", - "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", - "bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338", - "bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info", - "bundle": 1214857 + }, + "hello_world__closure": { + "master": { + "uncompressed": { + "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", + "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", + "bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338", + "bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info", + "bundle": 1214857 + } } } } diff --git a/packages/zone.js/lib/browser/api-util.ts b/packages/zone.js/lib/browser/api-util.ts index e9dff33932e..ed2259ef5fd 100644 --- a/packages/zone.js/lib/browser/api-util.ts +++ b/packages/zone.js/lib/browser/api-util.ts @@ -10,9 +10,12 @@ import {globalSources, patchEventPrototype, patchEventTarget, zoneSymbolEventNam import {ADD_EVENT_LISTENER_STR, ArraySlice, attachOriginToPatched, bindArguments, FALSE_STR, isBrowser, isIEOrEdge, isMix, isNode, ObjectCreate, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, patchClass, patchMacroTask, patchMethod, patchOnProperties, REMOVE_EVENT_LISTENER_STR, TRUE_STR, wrapWithCurrentZone, ZONE_SYMBOL_PREFIX} from '../common/utils'; import {patchCallbacks} from './browser-util'; -import {eventNames, filterProperties} from './property-descriptor'; +import {filterProperties, getOnEventNames} from './property-descriptor'; Zone.__load_patch('util', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // Collect native event names by looking at properties + // on the global namespace, e.g. 'onclick'. + const eventNames: string[] = getOnEventNames(global); api.patchOnProperties = patchOnProperties; api.patchMethod = patchMethod; api.bindArguments = bindArguments; diff --git a/packages/zone.js/lib/browser/property-descriptor.ts b/packages/zone.js/lib/browser/property-descriptor.ts index 2eaf64376c8..cc08e692df0 100644 --- a/packages/zone.js/lib/browser/property-descriptor.ts +++ b/packages/zone.js/lib/browser/property-descriptor.ts @@ -12,226 +12,6 @@ import {isBrowser, isIE, isMix, isNode, ObjectGetPrototypeOf, patchOnProperties} from '../common/utils'; -const globalEventHandlersEventNames = [ - 'abort', - 'animationcancel', - 'animationend', - 'animationiteration', - 'auxclick', - 'beforeinput', - 'blur', - 'cancel', - 'canplay', - 'canplaythrough', - 'change', - 'compositionstart', - 'compositionupdate', - 'compositionend', - 'cuechange', - 'click', - 'close', - 'contextmenu', - 'curechange', - 'dblclick', - 'drag', - 'dragend', - 'dragenter', - 'dragexit', - 'dragleave', - 'dragover', - 'drop', - 'durationchange', - 'emptied', - 'ended', - 'error', - 'focus', - 'focusin', - 'focusout', - 'gotpointercapture', - 'input', - 'invalid', - 'keydown', - 'keypress', - 'keyup', - 'load', - 'loadstart', - 'loadeddata', - 'loadedmetadata', - 'lostpointercapture', - 'mousedown', - 'mouseenter', - 'mouseleave', - 'mousemove', - 'mouseout', - 'mouseover', - 'mouseup', - 'mousewheel', - 'orientationchange', - 'pause', - 'play', - 'playing', - 'pointercancel', - 'pointerdown', - 'pointerenter', - 'pointerleave', - 'pointerlockchange', - 'mozpointerlockchange', - 'webkitpointerlockerchange', - 'pointerlockerror', - 'mozpointerlockerror', - 'webkitpointerlockerror', - 'pointermove', - 'pointout', - 'pointerover', - 'pointerup', - 'progress', - 'ratechange', - 'reset', - 'resize', - 'scroll', - 'seeked', - 'seeking', - 'select', - 'selectionchange', - 'selectstart', - 'show', - 'sort', - 'stalled', - 'submit', - 'suspend', - 'timeupdate', - 'volumechange', - 'touchcancel', - 'touchmove', - 'touchstart', - 'touchend', - 'transitioncancel', - 'transitionend', - 'waiting', - 'wheel' -]; -const documentEventNames = [ - 'afterscriptexecute', 'beforescriptexecute', 'DOMContentLoaded', 'freeze', 'fullscreenchange', - 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange', 'fullscreenerror', - 'mozfullscreenerror', 'webkitfullscreenerror', 'msfullscreenerror', 'readystatechange', - 'visibilitychange', 'resume' -]; -const windowEventNames = [ - 'absolutedeviceorientation', - 'afterinput', - 'afterprint', - 'appinstalled', - 'beforeinstallprompt', - 'beforeprint', - 'beforeunload', - 'devicelight', - 'devicemotion', - 'deviceorientation', - 'deviceorientationabsolute', - 'deviceproximity', - 'hashchange', - 'languagechange', - 'message', - 'mozbeforepaint', - 'offline', - 'online', - 'paint', - 'pageshow', - 'pagehide', - 'popstate', - 'rejectionhandled', - 'storage', - 'unhandledrejection', - 'unload', - 'userproximity', - 'vrdisplayconnected', - 'vrdisplaydisconnected', - 'vrdisplaypresentchange' -]; -const htmlElementEventNames = [ - 'beforecopy', 'beforecut', 'beforepaste', 'copy', 'cut', 'paste', 'dragstart', 'loadend', - 'animationstart', 'search', 'transitionrun', 'transitionstart', 'webkitanimationend', - 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend' -]; -const mediaElementEventNames = - ['encrypted', 'waitingforkey', 'msneedkey', 'mozinterruptbegin', 'mozinterruptend']; -const ieElementEventNames = [ - 'activate', - 'afterupdate', - 'ariarequest', - 'beforeactivate', - 'beforedeactivate', - 'beforeeditfocus', - 'beforeupdate', - 'cellchange', - 'controlselect', - 'dataavailable', - 'datasetchanged', - 'datasetcomplete', - 'errorupdate', - 'filterchange', - 'layoutcomplete', - 'losecapture', - 'move', - 'moveend', - 'movestart', - 'propertychange', - 'resizeend', - 'resizestart', - 'rowenter', - 'rowexit', - 'rowsdelete', - 'rowsinserted', - 'command', - 'compassneedscalibration', - 'deactivate', - 'help', - 'mscontentzoom', - 'msmanipulationstatechanged', - 'msgesturechange', - 'msgesturedoubletap', - 'msgestureend', - 'msgesturehold', - 'msgesturestart', - 'msgesturetap', - 'msgotpointercapture', - 'msinertiastart', - 'mslostpointercapture', - 'mspointercancel', - 'mspointerdown', - 'mspointerenter', - 'mspointerhover', - 'mspointerleave', - 'mspointermove', - 'mspointerout', - 'mspointerover', - 'mspointerup', - 'pointerout', - 'mssitemodejumplistitemremoved', - 'msthumbnailclick', - 'stop', - 'storagecommit' -]; -const webglEventNames = ['webglcontextrestored', 'webglcontextlost', 'webglcontextcreationerror']; -const formEventNames = ['autocomplete', 'autocompleteerror']; -const detailEventNames = ['toggle']; -const frameEventNames = ['load']; -const frameSetEventNames = ['blur', 'error', 'focus', 'load', 'resize', 'scroll', 'messageerror']; -const marqueeEventNames = ['bounce', 'finish', 'start']; - -const XMLHttpRequestEventNames = [ - 'loadstart', 'progress', 'abort', 'error', 'load', 'progress', 'timeout', 'loadend', - 'readystatechange' -]; -const IDBIndexEventNames = - ['upgradeneeded', 'complete', 'abort', 'success', 'error', 'blocked', 'versionchange', 'close']; -const websocketEventNames = ['close', 'error', 'open', 'message']; -const workerEventNames = ['error', 'message']; - -export const eventNames = globalEventHandlersEventNames.concat( - webglEventNames, formEventNames, detailEventNames, documentEventNames, windowEventNames, - htmlElementEventNames, ieElementEventNames); - export interface IgnoreProperty { target: any; ignoreProperties: string[]; @@ -263,6 +43,16 @@ export function patchFilteredProperties( patchOnProperties(target, filteredProperties, prototype); } +/** + * Get all event name properties which the event name startsWith `on` + * from the target object itself, inherited properties are not considered. + */ +export function getOnEventNames(target: Object) { + return Object.getOwnPropertyNames(target) + .filter(name => name.startsWith('on') && name.length > 2) + .map(name => name.substring(2)); +} + export function propertyDescriptorPatch(api: _ZonePrivate, _global: any) { if (isNode && !isMix) { return; @@ -271,64 +61,32 @@ export function propertyDescriptorPatch(api: _ZonePrivate, _global: any) { // events are already been patched by legacy patch. return; } - const supportsWebSocket = typeof WebSocket !== 'undefined'; const ignoreProperties: IgnoreProperty[] = _global['__Zone_ignore_on_properties']; // for browsers that we can patch the descriptor: Chrome & Firefox + let patchTargets: string[] = []; if (isBrowser) { const internalWindow: any = window; + patchTargets = patchTargets.concat([ + 'Document', 'SVGElement', 'Element', 'HTMLElement', 'HTMLBodyElement', 'HTMLMediaElement', + 'HTMLFrameSetElement', 'HTMLFrameElement', 'HTMLIFrameElement', 'HTMLMarqueeElement', 'Worker' + ]); const ignoreErrorProperties = isIE() ? [{target: internalWindow, ignoreProperties: ['error']}] : []; // in IE/Edge, onProp not exist in window object, but in WindowPrototype // so we need to pass WindowPrototype to check onProp exist or not patchFilteredProperties( - internalWindow, eventNames.concat(['messageerror']), + internalWindow, getOnEventNames(internalWindow), ignoreProperties ? ignoreProperties.concat(ignoreErrorProperties) : ignoreProperties, ObjectGetPrototypeOf(internalWindow)); - patchFilteredProperties(Document.prototype, eventNames, ignoreProperties); - - if (typeof internalWindow['SVGElement'] !== 'undefined') { - patchFilteredProperties(internalWindow['SVGElement'].prototype, eventNames, ignoreProperties); - } - patchFilteredProperties(Element.prototype, eventNames, ignoreProperties); - patchFilteredProperties(HTMLElement.prototype, eventNames, ignoreProperties); - patchFilteredProperties(HTMLMediaElement.prototype, mediaElementEventNames, ignoreProperties); - patchFilteredProperties( - HTMLFrameSetElement.prototype, windowEventNames.concat(frameSetEventNames), - ignoreProperties); - patchFilteredProperties( - HTMLBodyElement.prototype, windowEventNames.concat(frameSetEventNames), ignoreProperties); - patchFilteredProperties(HTMLFrameElement.prototype, frameEventNames, ignoreProperties); - patchFilteredProperties(HTMLIFrameElement.prototype, frameEventNames, ignoreProperties); - - const HTMLMarqueeElement = internalWindow['HTMLMarqueeElement']; - if (HTMLMarqueeElement) { - patchFilteredProperties(HTMLMarqueeElement.prototype, marqueeEventNames, ignoreProperties); - } - const Worker = internalWindow['Worker']; - if (Worker) { - patchFilteredProperties(Worker.prototype, workerEventNames, ignoreProperties); - } } - const XMLHttpRequest = _global['XMLHttpRequest']; - if (XMLHttpRequest) { - // XMLHttpRequest is not available in ServiceWorker, so we need to check here - patchFilteredProperties(XMLHttpRequest.prototype, XMLHttpRequestEventNames, ignoreProperties); - } - const XMLHttpRequestEventTarget = _global['XMLHttpRequestEventTarget']; - if (XMLHttpRequestEventTarget) { - patchFilteredProperties( - XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype, XMLHttpRequestEventNames, - ignoreProperties); - } - if (typeof IDBIndex !== 'undefined') { - patchFilteredProperties(IDBIndex.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBRequest.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBOpenDBRequest.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBDatabase.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBTransaction.prototype, IDBIndexEventNames, ignoreProperties); - patchFilteredProperties(IDBCursor.prototype, IDBIndexEventNames, ignoreProperties); - } - if (supportsWebSocket) { - patchFilteredProperties(WebSocket.prototype, websocketEventNames, ignoreProperties); + patchTargets = patchTargets.concat([ + 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'IDBIndex', 'IDBRequest', 'IDBOpenDBRequest', + 'IDBDatabase', 'IDBTransaction', 'IDBCursor', 'WebSocket' + ]); + for (let i = 0; i < patchTargets.length; i++) { + const target = _global[patchTargets[i]]; + target && target.prototype && + patchFilteredProperties( + target.prototype, getOnEventNames(target.prototype), ignoreProperties); } } diff --git a/packages/zone.js/lib/common/utils.ts b/packages/zone.js/lib/common/utils.ts index 28a58f0ff16..ab0d79e9ba4 100644 --- a/packages/zone.js/lib/common/utils.ts +++ b/packages/zone.js/lib/common/utils.ts @@ -207,22 +207,20 @@ export function patchProperty(obj: any, prop: string, prototype?: any) { if (!target) { return; } - let previousValue = (target as any)[eventNameSymbol]; - if (previousValue) { + + const previousValue = (target as any)[eventNameSymbol]; + if (previousValue && typeof previousValue === 'function') { target.removeEventListener(eventName, wrapFn); } - // issue #978, when onload handler was added before loading zone.js - // we should remove it with originalDescSet - if (originalDescSet) { - originalDescSet.apply(target, NULL_ON_PROP_VALUE); - } - + (target as any)[eventNameSymbol] = newValue; if (typeof newValue === 'function') { - (target as any)[eventNameSymbol] = newValue; + // issue #978, when onload handler was added before loading zone.js + // we should remove it with originalDescSet + if (originalDescSet) { + originalDescSet.apply(target, NULL_ON_PROP_VALUE); + } target.addEventListener(eventName, wrapFn, false); - } else { - (target as any)[eventNameSymbol] = null; } }; diff --git a/packages/zone.js/test/assets/empty-worker.js b/packages/zone.js/test/assets/empty-worker.js new file mode 100644 index 00000000000..823e9bf40a2 --- /dev/null +++ b/packages/zone.js/test/assets/empty-worker.js @@ -0,0 +1,7 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index a828a25c37f..f8f7d87f4f6 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -138,17 +138,265 @@ describe('Zone', function() { hookSpy = jasmine.createSpy('hook'); eventListenerSpy = jasmine.createSpy('eventListener'); }); + const globalEventHandlersEventNames = [ + 'abort', + 'animationcancel', + 'animationend', + 'animationiteration', + 'auxclick', + 'beforeinput', + 'blur', + 'cancel', + 'canplay', + 'canplaythrough', + 'change', + 'compositionstart', + 'compositionupdate', + 'compositionend', + 'cuechange', + 'click', + 'close', + 'contextmenu', + 'curechange', + 'dblclick', + 'drag', + 'dragend', + 'dragenter', + 'dragexit', + 'dragleave', + 'dragover', + 'drop', + 'durationchange', + 'emptied', + 'ended', + 'error', + 'focus', + 'focusin', + 'focusout', + 'gotpointercapture', + 'input', + 'invalid', + 'keydown', + 'keypress', + 'keyup', + 'load', + 'loadstart', + 'loadeddata', + 'loadedmetadata', + 'lostpointercapture', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'mousewheel', + 'orientationchange', + 'pause', + 'play', + 'playing', + 'pointercancel', + 'pointerdown', + 'pointerenter', + 'pointerleave', + 'pointerlockchange', + 'mozpointerlockchange', + 'webkitpointerlockerchange', + 'pointerlockerror', + 'mozpointerlockerror', + 'webkitpointerlockerror', + 'pointermove', + 'pointout', + 'pointerover', + 'pointerup', + 'progress', + 'ratechange', + 'reset', + 'resize', + 'scroll', + 'seeked', + 'seeking', + 'select', + 'selectionchange', + 'selectstart', + 'show', + 'sort', + 'stalled', + 'submit', + 'suspend', + 'timeupdate', + 'volumechange', + 'touchcancel', + 'touchmove', + 'touchstart', + 'touchend', + 'transitioncancel', + 'transitionend', + 'waiting', + 'wheel' + ]; + const documentEventNames = [ + 'afterscriptexecute', 'beforescriptexecute', 'DOMContentLoaded', 'freeze', + 'fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', + 'msfullscreenchange', 'fullscreenerror', 'mozfullscreenerror', 'webkitfullscreenerror', + 'msfullscreenerror', 'readystatechange', 'visibilitychange', 'resume' + ]; + const windowEventNames = [ + 'absolutedeviceorientation', + 'afterinput', + 'afterprint', + 'appinstalled', + 'beforeinstallprompt', + 'beforeprint', + 'beforeunload', + 'devicelight', + 'devicemotion', + 'deviceorientation', + 'deviceorientationabsolute', + 'deviceproximity', + 'hashchange', + 'languagechange', + 'message', + 'mozbeforepaint', + 'offline', + 'online', + 'paint', + 'pageshow', + 'pagehide', + 'popstate', + 'rejectionhandled', + 'storage', + 'unhandledrejection', + 'unload', + 'userproximity', + 'vrdisplayconnected', + 'vrdisplaydisconnected', + 'vrdisplaypresentchange' + ]; + const htmlElementEventNames = [ + 'beforecopy', 'beforecut', 'beforepaste', 'copy', 'cut', 'paste', 'dragstart', + 'loadend', 'animationstart', 'search', 'transitionrun', 'transitionstart', + 'webkitanimationend', 'webkitanimationiteration', 'webkitanimationstart', + 'webkittransitionend' + ]; + const mediaElementEventNames = + ['encrypted', 'waitingforkey', 'msneedkey', 'mozinterruptbegin', 'mozinterruptend']; + const ieElementEventNames = [ + 'activate', + 'afterupdate', + 'ariarequest', + 'beforeactivate', + 'beforedeactivate', + 'beforeeditfocus', + 'beforeupdate', + 'cellchange', + 'controlselect', + 'dataavailable', + 'datasetchanged', + 'datasetcomplete', + 'errorupdate', + 'filterchange', + 'layoutcomplete', + 'losecapture', + 'move', + 'moveend', + 'movestart', + 'propertychange', + 'resizeend', + 'resizestart', + 'rowenter', + 'rowexit', + 'rowsdelete', + 'rowsinserted', + 'command', + 'compassneedscalibration', + 'deactivate', + 'help', + 'mscontentzoom', + 'msmanipulationstatechanged', + 'msgesturechange', + 'msgesturedoubletap', + 'msgestureend', + 'msgesturehold', + 'msgesturestart', + 'msgesturetap', + 'msgotpointercapture', + 'msinertiastart', + 'mslostpointercapture', + 'mspointercancel', + 'mspointerdown', + 'mspointerenter', + 'mspointerhover', + 'mspointerleave', + 'mspointermove', + 'mspointerout', + 'mspointerover', + 'mspointerup', + 'pointerout', + 'mssitemodejumplistitemremoved', + 'msthumbnailclick', + 'stop', + 'storagecommit' + ]; + const webglEventNames = + ['webglcontextrestored', 'webglcontextlost', 'webglcontextcreationerror']; + const formEventNames = ['autocomplete', 'autocompleteerror']; + const detailEventNames = ['toggle']; + const frameEventNames = ['load']; + const frameSetEventNames = + ['blur', 'error', 'focus', 'load', 'resize', 'scroll', 'messageerror']; + const marqueeEventNames = ['bounce', 'finish', 'start']; - function checkIsOnPropertiesPatched(target: any, ignoredProperties?: string[]) { - for (let prop in target) { + const XMLHttpRequestEventNames = [ + 'loadstart', 'progress', 'abort', 'error', 'load', 'progress', 'timeout', 'loadend', + 'readystatechange' + ]; + const IDBIndexEventNames = [ + 'upgradeneeded', 'complete', 'abort', 'success', 'error', 'blocked', 'versionchange', + 'close' + ]; + const websocketEventNames = ['close', 'error', 'open', 'message']; + const workerEventNames = ['error', 'message']; + + const eventNames = globalEventHandlersEventNames.concat( + webglEventNames, formEventNames, detailEventNames, documentEventNames, + windowEventNames, htmlElementEventNames, ieElementEventNames); + + + function checkIsOnPropertiesPatched( + target: any, shouldPatchedProperties?: string[], ignoredProperties?: string[]) { + let checkTargetProps = + shouldPatchedProperties && shouldPatchedProperties.map(p => `on${p}`); + if (!checkTargetProps) { + checkTargetProps = []; + for (let prop in target) { + checkTargetProps.push(prop); + } + } + for (let i = 0; i < checkTargetProps.length; i++) { + const prop = checkTargetProps[i]; if (ignoredProperties && ignoredProperties.filter(ignoreProp => ignoreProp === prop).length > 0) { continue; } if (prop.substr(0, 2) === 'on' && prop.length > 2) { + let propExistsOnTarget = false; + let checkTarget = target; + while (checkTarget && checkTarget !== Object) { + const desc = Object.getOwnPropertyDescriptor(checkTarget, prop); + if (desc && desc.configurable) { + propExistsOnTarget = true; + break; + } + checkTarget = Object.getPrototypeOf(checkTarget); + } + if (!propExistsOnTarget) { + continue; + } target[prop] = noop; if (!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]) { - console.log('onProp is null:', prop); + fail(`${prop} of ${target} is not patched`); } else { target[prop] = null; expect(!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]).toBeTruthy(); @@ -157,7 +405,90 @@ describe('Zone', function() { } } - it('should patch all possbile on properties on element', function() { + it('should patch all possible on properties on native prototype', function() { + function isPropertyPatched(obj: any, prop: string, prototype?: any) { + let desc = Object.getOwnPropertyDescriptor(obj, prop); + if (!desc && prototype) { + // when patch window object, use prototype to check prop exist or not + const prototypeDesc = Object.getOwnPropertyDescriptor(prototype, prop); + if (prototypeDesc) { + desc = {enumerable: true, configurable: true}; + } + } + // if the descriptor not exists or is not configurable + // just return + if (!desc || !desc.configurable) { + return true; + } + + const onPropPatchedSymbol = zoneSymbol('on' + prop + 'patched'); + if (obj.hasOwnProperty(onPropPatchedSymbol) && obj[onPropPatchedSymbol]) { + return true; + } + return false; + } + + function isPropertiesPatched(obj: any, properties: string[]|null, prototype?: any) { + if (!properties) { + return []; + } + for (let i = 0; i < properties.length; i++) { + if (!isPropertyPatched(obj, 'on' + properties[i], prototype)) { + fail(`${properties[i]} is not patched on ${obj}`); + } + } + } + isPropertiesPatched( + window, eventNames.concat(['messageerror']), Object.getPrototypeOf(window)); + isPropertiesPatched(Document.prototype, eventNames); + + if (typeof window['SVGElement'] !== 'undefined') { + isPropertiesPatched(window['SVGElement'].prototype, eventNames); + } + isPropertiesPatched(Element.prototype, eventNames); + isPropertiesPatched(HTMLElement.prototype, eventNames); + isPropertiesPatched(HTMLMediaElement.prototype, mediaElementEventNames); + isPropertiesPatched( + HTMLFrameSetElement.prototype, windowEventNames.concat(frameSetEventNames)); + isPropertiesPatched( + HTMLBodyElement.prototype, windowEventNames.concat(frameSetEventNames)); + isPropertiesPatched(HTMLFrameElement.prototype, frameEventNames); + isPropertiesPatched(HTMLIFrameElement.prototype, frameEventNames); + + const HTMLMarqueeElement = window['HTMLMarqueeElement']; + if (HTMLMarqueeElement) { + isPropertiesPatched(HTMLMarqueeElement.prototype, marqueeEventNames); + } + const Worker = window['Worker']; + if (Worker) { + isPropertiesPatched(Worker.prototype, workerEventNames); + } + const XMLHttpRequest = window['XMLHttpRequest']; + if (XMLHttpRequest) { + // XMLHttpRequest is not available in ServiceWorker, so we need to check here + isPropertiesPatched(XMLHttpRequest.prototype, XMLHttpRequestEventNames); + } + const XMLHttpRequestEventTarget = window['XMLHttpRequestEventTarget']; + if (XMLHttpRequestEventTarget) { + isPropertiesPatched( + XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype, + XMLHttpRequestEventNames); + } + if (typeof IDBIndex !== 'undefined') { + isPropertiesPatched(IDBIndex.prototype, IDBIndexEventNames); + isPropertiesPatched(IDBRequest.prototype, IDBIndexEventNames); + isPropertiesPatched(IDBOpenDBRequest.prototype, IDBIndexEventNames); + isPropertiesPatched(IDBDatabase.prototype, IDBIndexEventNames); + isPropertiesPatched(IDBTransaction.prototype, IDBIndexEventNames); + isPropertiesPatched(IDBCursor.prototype, IDBIndexEventNames); + } + const WebSocket = window['WebSocket']; + if (WebSocket) { + isPropertiesPatched(WebSocket.prototype, websocketEventNames); + } + }); + + it('should patch all possible on properties on element', function() { const htmlElementTagNames: string[] = [ 'a', 'area', 'audio', 'base', 'basefont', 'blockquote', 'br', 'button', 'canvas', 'caption', 'col', 'colgroup', 'data', 'datalist', @@ -173,29 +504,60 @@ describe('Zone', function() { 'video' ]; htmlElementTagNames.forEach(tagName => { - checkIsOnPropertiesPatched(document.createElement(tagName), ['onorientationchange']); + checkIsOnPropertiesPatched( + document.createElement(tagName), eventNames, ['onorientationchange']); }); }); - it('should patch all possbile on properties on body', function() { - checkIsOnPropertiesPatched(document.body, ['onorientationchange']); + it('should patch all possible on properties on svg element', function() { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + checkIsOnPropertiesPatched(svg, eventNames); }); - it('should patch all possbile on properties on Document', function() { - checkIsOnPropertiesPatched(document, ['onorientationchange']); + it('should patch all possible on properties on media element', function() { + const audio = document.createElement('audio'); + checkIsOnPropertiesPatched(audio, mediaElementEventNames); }); - it('should patch all possbile on properties on Window', function() { - checkIsOnPropertiesPatched(window, [ - 'onvrdisplayactivate', 'onvrdisplayblur', 'onvrdisplayconnect', - 'onvrdisplaydeactivate', 'onvrdisplaydisconnect', 'onvrdisplayfocus', - 'onvrdisplaypointerrestricted', 'onvrdisplaypointerunrestricted', - 'onorientationchange', 'onerror' - ]); + it('should patch all possible on properties on frameset element', function() { + const frameset = document.createElement('frameset'); + checkIsOnPropertiesPatched(frameset, windowEventNames.concat(frameSetEventNames)); }); - it('should patch all possbile on properties on xhr', function() { - checkIsOnPropertiesPatched(new XMLHttpRequest()); + it('should patch all possible on properties on body', function() { + checkIsOnPropertiesPatched(document.body, windowEventNames.concat(frameSetEventNames)); + }); + + it('should patch all possible on properties on Frame', function() { + const frame = document.createElement('frame'); + checkIsOnPropertiesPatched(frame, frameEventNames); + }); + + it('should patch all possible on properties on marquee element', function() { + const marquee = document.createElement('marquee'); + checkIsOnPropertiesPatched(marquee, marqueeEventNames); + }); + + it('should patch all possible on properties on Window', function() { + checkIsOnPropertiesPatched(window, eventNames.concat(['messageerror'])); + }); + + it('should patch all possible on properties on xhr', function() { + checkIsOnPropertiesPatched(new XMLHttpRequest(), XMLHttpRequestEventNames); + }); + + it('should patch all possible on properties on worker', function() { + checkIsOnPropertiesPatched( + new Worker('/base/angular/packages/zone.js/test/assets/worker.js'), + workerEventNames); + }); + + it('should patch all possible on properties on websocket', function() { + try { + checkIsOnPropertiesPatched(new WebSocket('ws://localhost:8001'), websocketEventNames); + } catch (e) { + console.log('error when creating websocket', e); + } }); it('should not patch ignored on properties', function() { @@ -242,6 +604,16 @@ describe('Zone', function() { document.body.removeChild(div); }); + it('property startsWith on but not event listener should still work as expected', () => { + (window as any).one_two_three = {foo: 'bar'}; + + expect((window as any).one_two_three).toEqual({foo: 'bar'}); + (window as any).one_two_three = {bar: 'foo'}; + expect((window as any).one_two_three).toEqual({bar: 'foo'}); + (window as any).one_two_three = null; + expect((window as any).one_two_three).toBeNull(); + }); + it('should be able to clear on handler added before load zone.js', function() { const TestTarget: any = (window as any)['TestTarget']; patchFilteredProperties(