perf(core): use native addEventListener for faster rendering. (#18107)
Angular can make many assumptions about its event handlers. As a result the bookkeeping for native addEventListener is significantly cheaper than Zone's addEventLister which can't make such assumptions. This change bypasses the Zone's addEventListener if present and always uses the native addEventHandler. As a result registering event listeners is about 3 times faster. PR Close #18107
This commit is contained in:

committed by
Miško Hevery

parent
8bcb268140
commit
6279e50d78
@ -189,14 +189,7 @@ export function listenToElementOutputs(view: ViewData, compView: ViewData, def:
|
||||
}
|
||||
|
||||
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
||||
return (event: any) => {
|
||||
try {
|
||||
return dispatchEvent(view, index, eventName, event);
|
||||
} catch (e) {
|
||||
// Attention: Don't rethrow, to keep in sync with directive events.
|
||||
view.root.errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
return (event: any) => dispatchEvent(view, index, eventName, event);
|
||||
}
|
||||
|
||||
|
||||
|
@ -138,14 +138,7 @@ export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
|
||||
}
|
||||
|
||||
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
||||
return (event: any) => {
|
||||
try {
|
||||
return dispatchEvent(view, index, eventName, event);
|
||||
} catch (e) {
|
||||
// Attention: Don't rethrow, as it would cancel Observable subscriptions!
|
||||
view.root.errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
return (event: any) => dispatchEvent(view, index, eventName, event);
|
||||
}
|
||||
|
||||
export function checkAndUpdateDirectiveInline(
|
||||
|
@ -126,12 +126,18 @@ export function markParentViewsForCheckProjectedViews(view: ViewData, endView: V
|
||||
}
|
||||
|
||||
export function dispatchEvent(
|
||||
view: ViewData, nodeIndex: number, eventName: string, event: any): boolean {
|
||||
const nodeDef = view.def.nodes[nodeIndex];
|
||||
const startView =
|
||||
nodeDef.flags & NodeFlags.ComponentView ? asElementData(view, nodeIndex).componentView : view;
|
||||
markParentViewsForCheck(startView);
|
||||
return Services.handleEvent(view, nodeIndex, eventName, event);
|
||||
view: ViewData, nodeIndex: number, eventName: string, event: any): boolean|undefined {
|
||||
try {
|
||||
const nodeDef = view.def.nodes[nodeIndex];
|
||||
const startView = nodeDef.flags & NodeFlags.ComponentView ?
|
||||
asElementData(view, nodeIndex).componentView :
|
||||
view;
|
||||
markParentViewsForCheck(startView);
|
||||
return Services.handleEvent(view, nodeIndex, eventName, event);
|
||||
} catch (e) {
|
||||
// Attention: Don't rethrow, as it would cancel Observable subscriptions!
|
||||
view.root.errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function declaredViewContainer(view: ViewData): ElementData|null {
|
||||
|
@ -164,13 +164,39 @@ export class NgZone {
|
||||
*
|
||||
* If a synchronous error happens it will be rethrown and not reported via `onError`.
|
||||
*/
|
||||
run(fn: () => any): any { return (this as any as NgZonePrivate)._inner.run(fn); }
|
||||
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T {
|
||||
return (this as any as NgZonePrivate)._inner.run(fn, applyThis, applyArgs) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the `fn` function synchronously within the Angular zone as a task and returns value
|
||||
* returned by the function.
|
||||
*
|
||||
* Running functions via `run` allows you to reenter Angular zone from a task that was executed
|
||||
* outside of the Angular zone (typically started via {@link #runOutsideAngular}).
|
||||
*
|
||||
* Any future tasks or microtasks scheduled from within this function will continue executing from
|
||||
* within the Angular zone.
|
||||
*
|
||||
* If a synchronous error happens it will be rethrown and not reported via `onError`.
|
||||
*/
|
||||
runTask<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], name?: string): T {
|
||||
const zone = (this as any as NgZonePrivate)._inner;
|
||||
const task = zone.scheduleEventTask('NgZoneEvent: ' + name, fn, EMPTY_PAYLOAD, noop, noop);
|
||||
try {
|
||||
return zone.runTask(task, applyThis, applyArgs) as T;
|
||||
} finally {
|
||||
zone.cancelTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `run`, except that synchronous errors are caught and forwarded via `onError` and not
|
||||
* rethrown.
|
||||
*/
|
||||
runGuarded(fn: () => any): any { return (this as any as NgZonePrivate)._inner.runGuarded(fn); }
|
||||
runGuarded<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T {
|
||||
return (this as any as NgZonePrivate)._inner.runGuarded(fn, applyThis, applyArgs) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the `fn` function synchronously in Angular's parent zone and returns value returned by
|
||||
@ -185,9 +211,15 @@ export class NgZone {
|
||||
*
|
||||
* Use {@link #run} to reenter the Angular zone and do work that updates the application model.
|
||||
*/
|
||||
runOutsideAngular(fn: () => any): any { return (this as any as NgZonePrivate)._outer.run(fn); }
|
||||
runOutsideAngular<T>(fn: (...args: any[]) => T): T {
|
||||
return (this as any as NgZonePrivate)._outer.run(fn) as T;
|
||||
}
|
||||
}
|
||||
|
||||
function noop(){};
|
||||
const EMPTY_PAYLOAD = {};
|
||||
|
||||
|
||||
interface NgZonePrivate extends NgZone {
|
||||
_outer: Zone;
|
||||
_inner: Zone;
|
||||
|
@ -12,6 +12,12 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
import {createRootView, isBrowser, recordNodeToRemove} from './helper';
|
||||
|
||||
/**
|
||||
* We map addEventListener to the Zones internal name. This is because we want to be fast
|
||||
* and bypass the zone bookkeeping. We know that we can do the bookkeeping faster.
|
||||
*/
|
||||
const addEventListener = '__zone_symbol__addEventListener';
|
||||
|
||||
export function main() {
|
||||
describe(`Component Views`, () => {
|
||||
function compViewDef(
|
||||
@ -180,7 +186,7 @@ export function main() {
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
@ -301,4 +307,4 @@ export function main() {
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,13 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, recordNodeToRemove} from './helper';
|
||||
|
||||
/**
|
||||
* We map addEventListener to the Zones internal name. This is because we want to be fast
|
||||
* and bypass the zone bookkeeping. We know that we can do the bookkeeping faster.
|
||||
*/
|
||||
const addEventListener = '__zone_symbol__addEventListener';
|
||||
const removeEventListener = '__zone_symbol__removeEventListener';
|
||||
|
||||
export function main() {
|
||||
describe(`View Elements`, () => {
|
||||
function compViewDef(
|
||||
@ -190,7 +197,7 @@ export function main() {
|
||||
it('should listen to DOM events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const removeListenerSpy =
|
||||
spyOn(HTMLElement.prototype, 'removeEventListener').and.callThrough();
|
||||
spyOn(HTMLElement.prototype, removeEventListener).and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null !, null !, 0, 'button', null !, null !, [[null !, 'click']],
|
||||
handleEventSpy)]));
|
||||
@ -210,8 +217,8 @@ export function main() {
|
||||
|
||||
it('should listen to window events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const addListenerSpy = spyOn(window, 'addEventListener');
|
||||
const removeListenerSpy = spyOn(window, 'removeEventListener');
|
||||
const addListenerSpy = spyOn(window, addEventListener);
|
||||
const removeListenerSpy = spyOn(window, removeEventListener);
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null !, null !, 0, 'button', null !, null !,
|
||||
[['window', 'windowClick']], handleEventSpy)]));
|
||||
@ -233,8 +240,8 @@ export function main() {
|
||||
|
||||
it('should listen to document events', () => {
|
||||
const handleEventSpy = jasmine.createSpy('handleEvent');
|
||||
const addListenerSpy = spyOn(document, 'addEventListener');
|
||||
const removeListenerSpy = spyOn(document, 'removeEventListener');
|
||||
const addListenerSpy = spyOn(document, addEventListener);
|
||||
const removeListenerSpy = spyOn(document, removeEventListener);
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null !, null !, 0, 'button', null !, null !,
|
||||
[['document', 'documentClick']], handleEventSpy)]));
|
||||
@ -284,7 +291,7 @@ export function main() {
|
||||
|
||||
it('should report debug info on event errors', () => {
|
||||
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null !, null !, 0, 'button', null !, null !, [[null !, 'click']],
|
||||
() => { throw new Error('Test'); })]));
|
||||
|
@ -220,6 +220,13 @@ function commonTests() {
|
||||
macroTask(() => { async.done(); });
|
||||
}), testTimeout);
|
||||
|
||||
it('should return the body return value from runTask',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
macroTask(() => { expect(_zone.runTask(() => 6)).toEqual(6); });
|
||||
|
||||
macroTask(() => { async.done(); });
|
||||
}), testTimeout);
|
||||
|
||||
it('should call onUnstable and onMicrotaskEmpty',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
runNgZoneNoLog(() => macroTask(_log.fn('run')));
|
||||
|
Reference in New Issue
Block a user