revert: feat: add a flag in bootstrap to enable coalesce event change detection to improve performance (#30533) (#33230)

This reverts commit 21c1e14385.

PR Close #33230
This commit is contained in:
Matias Niemelä 2019-10-17 08:48:37 -07:00
parent d192a7b47a
commit 082aed6e46
11 changed files with 21 additions and 171 deletions

View File

@ -21,7 +21,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 1440, "runtime": 1440,
"main": 125178, "main": 123904,
"polyfills": 45340 "polyfills": 45340
} }
} }

View File

@ -14,23 +14,6 @@ var __global = "undefined" !== typeof global && global;
var _global = __globalThis || __global || __window || __self; var _global = __globalThis || __global || __window || __self;
function getNativeRequestAnimationFrame() {
var nativeRequestAnimationFrame = _global["requestAnimationFrame"];
var nativeCancelAnimationFrame = _global["cancelAnimationFrame"];
if ("undefined" !== typeof Zone && nativeRequestAnimationFrame && nativeCancelAnimationFrame) {
var unpatchedRequestAnimationFrame = nativeRequestAnimationFrame[Zone.__symbol__("OriginalDelegate")];
if (unpatchedRequestAnimationFrame) nativeRequestAnimationFrame = unpatchedRequestAnimationFrame;
var unpatchedCancelAnimationFrame = nativeCancelAnimationFrame[Zone.__symbol__("OriginalDelegate")];
if (unpatchedCancelAnimationFrame) nativeCancelAnimationFrame = unpatchedCancelAnimationFrame;
}
return {
nativeRequestAnimationFrame: nativeRequestAnimationFrame,
nativeCancelAnimationFrame: nativeCancelAnimationFrame
};
}
var nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
if (ngDevMode) _global.$localize = _global.$localize || function() { if (ngDevMode) _global.$localize = _global.$localize || function() {
throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize/init';` to your polyfills.ts file."); throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize/init';` to your polyfills.ts file.");
}; };

View File

@ -219,27 +219,6 @@ export interface BootstrapOptions {
* - `noop` - Use `NoopNgZone` which does nothing. * - `noop` - Use `NoopNgZone` which does nothing.
*/ */
ngZone?: NgZone|'zone.js'|'noop'; ngZone?: NgZone|'zone.js'|'noop';
/**
* Optionally specify coalescing event change detections or not.
* Consider the following case.
*
* <div (click)="doSomething()">
* <button (click)="doSomethingElse()"></button>
* </div>
*
* When button is clicked, because of the event bubbling, both
* event handlers will be called and 2 change detections will be
* triggered. We can colesce such kind of events to only trigger
* change detection only once.
*
* By default, this option will be false. So the events will not be
* colesced and the change detection will be triggered multiple times.
* And if this option be set to true, the change detection will be
* triggered async by scheduling a animation frame. So in the case above,
* the change detection will only be trigged once.
*/
ngZoneEventCoalescing?: boolean;
} }
/** /**
@ -290,8 +269,7 @@ export class PlatformRef {
// So we create a mini parent injector that just contains the new NgZone and // So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory. // pass that as parent to the NgModuleFactory.
const ngZoneOption = options ? options.ngZone : undefined; const ngZoneOption = options ? options.ngZone : undefined;
const ngZoneEventCoalescing = (options && options.ngZoneEventCoalescing) || false; const ngZone = getNgZone(ngZoneOption);
const ngZone = getNgZone(ngZoneOption, ngZoneEventCoalescing);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}]; const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Attention: Don't use ApplicationRef.run here, // Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`! // as we want to be sure that all possible constructor calls are inside `ngZone.run`!
@ -387,17 +365,14 @@ export class PlatformRef {
get destroyed() { return this._destroyed; } get destroyed() { return this._destroyed; }
} }
function getNgZone( function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone {
ngZoneOption: NgZone | 'zone.js' | 'noop' | undefined, ngZoneEventCoalescing: boolean): NgZone {
let ngZone: NgZone; let ngZone: NgZone;
if (ngZoneOption === 'noop') { if (ngZoneOption === 'noop') {
ngZone = new NoopNgZone(); ngZone = new NoopNgZone();
} else { } else {
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({ ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) ||
enableLongStackTrace: isDevMode(), new NgZone({enableLongStackTrace: isDevMode()});
shouldCoalesceEventChangeDetection: ngZoneEventCoalescing
});
} }
return ngZone; return ngZone;
} }

View File

@ -1,29 +0,0 @@
/**
* @license
* Copyright Google Inc. 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
*/
import {global} from './global';
export function getNativeRequestAnimationFrame() {
let nativeRequestAnimationFrame: (callback: FrameRequestCallback) => number =
global['requestAnimationFrame'];
let nativeCancelAnimationFrame: (handle: number) => void = global['cancelAnimationFrame'];
if (typeof Zone !== 'undefined' && nativeRequestAnimationFrame && nativeCancelAnimationFrame) {
// use unpatched version of requestAnimationFrame(native delegate) if possible
// to avoid another Change detection
const unpatchedRequestAnimationFrame =
(nativeRequestAnimationFrame as any)[(Zone as any).__symbol__('OriginalDelegate')];
if (unpatchedRequestAnimationFrame) {
nativeRequestAnimationFrame = unpatchedRequestAnimationFrame;
}
const unpatchedCancelAnimationFrame =
(nativeCancelAnimationFrame as any)[(Zone as any).__symbol__('OriginalDelegate')];
if (unpatchedCancelAnimationFrame) {
nativeCancelAnimationFrame = unpatchedCancelAnimationFrame;
}
}
return {nativeRequestAnimationFrame, nativeCancelAnimationFrame};
}

View File

@ -7,9 +7,6 @@
*/ */
import {EventEmitter} from '../event_emitter'; import {EventEmitter} from '../event_emitter';
import {global} from '../util/global';
import {getNativeRequestAnimationFrame} from '../util/raf';
/** /**
* An injectable service for executing work inside or outside of the Angular zone. * An injectable service for executing work inside or outside of the Angular zone.
@ -86,11 +83,8 @@ import {getNativeRequestAnimationFrame} from '../util/raf';
* @publicApi * @publicApi
*/ */
export class NgZone { export class NgZone {
readonly hasPendingZoneMicrotasks: boolean = false;
readonly lastRequestAnimationFrameId: number = -1;
readonly shouldCoalesceEventChangeDetection: boolean = true;
readonly hasPendingMacrotasks: boolean = false;
readonly hasPendingMicrotasks: boolean = false; readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false;
/** /**
* Whether there are no outstanding microtasks or macrotasks. * Whether there are no outstanding microtasks or macrotasks.
@ -121,8 +115,7 @@ export class NgZone {
*/ */
readonly onError: EventEmitter<any> = new EventEmitter(false); readonly onError: EventEmitter<any> = new EventEmitter(false);
constructor({enableLongStackTrace = false}) {
constructor({enableLongStackTrace = false, shouldCoalesceEventChangeDetection = false}) {
if (typeof Zone == 'undefined') { if (typeof Zone == 'undefined') {
throw new Error(`In this configuration Angular requires Zone.js`); throw new Error(`In this configuration Angular requires Zone.js`);
} }
@ -145,7 +138,6 @@ export class NgZone {
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']); self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
} }
self.shouldCoalesceEventChangeDetection = shouldCoalesceEventChangeDetection;
forkInnerZoneWithAngularBehavior(self); forkInnerZoneWithAngularBehavior(self);
} }
@ -229,19 +221,16 @@ export class NgZone {
function noop() {} function noop() {}
const EMPTY_PAYLOAD = {}; const EMPTY_PAYLOAD = {};
const {nativeRequestAnimationFrame} = getNativeRequestAnimationFrame();
interface NgZonePrivate extends NgZone { interface NgZonePrivate extends NgZone {
_outer: Zone; _outer: Zone;
_inner: Zone; _inner: Zone;
_nesting: number; _nesting: number;
_hasPendingMicrotasks: boolean;
hasPendingMacrotasks: boolean;
hasPendingMicrotasks: boolean; hasPendingMicrotasks: boolean;
lastRequestAnimationFrameId: number; hasPendingMacrotasks: boolean;
isStable: boolean; isStable: boolean;
shouldCoalesceEventChangeDetection: boolean;
} }
function checkStable(zone: NgZonePrivate) { function checkStable(zone: NgZonePrivate) {
@ -262,35 +251,16 @@ function checkStable(zone: NgZonePrivate) {
} }
} }
function delayChangeDetectionForEvents(zone: NgZonePrivate) {
if (zone.lastRequestAnimationFrameId !== -1) {
return;
}
zone.lastRequestAnimationFrameId = nativeRequestAnimationFrame.call(global, () => {
zone.lastRequestAnimationFrameId = -1;
updateMicroTaskStatus(zone);
checkStable(zone);
});
updateMicroTaskStatus(zone);
}
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) { function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
const delayChangeDetectionForEventsDelegate = () => { delayChangeDetectionForEvents(zone); };
const maybeDelayChangeDetection = !!zone.shouldCoalesceEventChangeDetection &&
nativeRequestAnimationFrame && delayChangeDetectionForEventsDelegate;
zone._inner = zone._inner.fork({ zone._inner = zone._inner.fork({
name: 'angular', name: 'angular',
properties: properties: <any>{'isAngularZone': true},
<any>{'isAngularZone': true, 'maybeDelayChangeDetection': maybeDelayChangeDetection},
onInvokeTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any, onInvokeTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => { applyArgs: any): any => {
try { try {
onEnter(zone); onEnter(zone);
return delegate.invokeTask(target, task, applyThis, applyArgs); return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally { } finally {
if (maybeDelayChangeDetection && task.type === 'eventTask') {
maybeDelayChangeDetection();
}
onLeave(zone); onLeave(zone);
} }
}, },
@ -313,8 +283,7 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
// We are only interested in hasTask events which originate from our zone // We are only interested in hasTask events which originate from our zone
// (A child hasTask event is not interesting to us) // (A child hasTask event is not interesting to us)
if (hasTaskState.change == 'microTask') { if (hasTaskState.change == 'microTask') {
zone._hasPendingMicrotasks = hasTaskState.microTask; zone.hasPendingMicrotasks = hasTaskState.microTask;
updateMicroTaskStatus(zone);
checkStable(zone); checkStable(zone);
} else if (hasTaskState.change == 'macroTask') { } else if (hasTaskState.change == 'macroTask') {
zone.hasPendingMacrotasks = hasTaskState.macroTask; zone.hasPendingMacrotasks = hasTaskState.macroTask;
@ -330,15 +299,6 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
}); });
} }
function updateMicroTaskStatus(zone: NgZonePrivate) {
if (zone._hasPendingMicrotasks ||
(zone.shouldCoalesceEventChangeDetection && zone.lastRequestAnimationFrameId !== -1)) {
zone.hasPendingMicrotasks = true;
} else {
zone.hasPendingMicrotasks = false;
}
}
function onEnter(zone: NgZonePrivate) { function onEnter(zone: NgZonePrivate) {
zone._nesting++; zone._nesting++;
if (zone.isStable) { if (zone.isStable) {
@ -357,8 +317,6 @@ function onLeave(zone: NgZonePrivate) {
* to framework to perform rendering. * to framework to perform rendering.
*/ */
export class NoopNgZone implements NgZone { export class NoopNgZone implements NgZone {
readonly hasPendingZoneMicrotasks: boolean = false;
readonly lastRequestAnimationFrameId = -1;
readonly hasPendingMicrotasks: boolean = false; readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false; readonly hasPendingMacrotasks: boolean = false;
readonly isStable: boolean = true; readonly isStable: boolean = true;
@ -366,7 +324,6 @@ export class NoopNgZone implements NgZone {
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter(); readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter(); readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter(); readonly onError: EventEmitter<any> = new EventEmitter();
readonly shouldCoalesceEventChangeDetection: boolean = false;
run(fn: (...args: any[]) => any, applyThis?: any, applyArgs?: any): any { run(fn: (...args: any[]) => any, applyThis?: any, applyArgs?: any): any {
return fn.apply(applyThis, applyArgs); return fn.apply(applyThis, applyArgs);

View File

@ -95,7 +95,7 @@ const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'
resolvedPromise.then((_) => { throw new Error('async'); }); resolvedPromise.then((_) => { throw new Error('async'); });
flushMicrotasks(); flushMicrotasks();
})(); })();
}).toThrow(); }).toThrowError(/Uncaught \(in promise\): Error: async/);
}); });
it('should complain if a test throws an exception', () => { it('should complain if a test throws an exception', () => {

View File

@ -16,7 +16,7 @@ import {EventEmitter, Injectable, NgZone} from '@angular/core';
export class MockNgZone extends NgZone { export class MockNgZone extends NgZone {
onStable: EventEmitter<any> = new EventEmitter(false); onStable: EventEmitter<any> = new EventEmitter(false);
constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: false}); } constructor() { super({enableLongStackTrace: false}); }
run(fn: Function): any { return fn(); } run(fn: Function): any { return fn(); }

View File

@ -402,8 +402,7 @@ export class TestBedViewEngine implements TestBed {
overrideComponentView(component, compFactory); overrideComponentView(component, compFactory);
} }
const ngZone = const ngZone = new NgZone({enableLongStackTrace: true});
new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false});
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}]; const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
const ngZoneInjector = Injector.create({ const ngZoneInjector = Injector.create({
providers: providers, providers: providers,

View File

@ -20,6 +20,7 @@ import {createMouseEvent, el} from '../../../testing/src/browser_util';
let zone: NgZone; let zone: NgZone;
describe('EventManager', () => { describe('EventManager', () => {
beforeEach(() => { beforeEach(() => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
zone = new NgZone({}); zone = new NgZone({});
@ -295,7 +296,7 @@ import {createMouseEvent, el} from '../../../testing/src/browser_util';
expect(receivedEvents).toEqual([]); expect(receivedEvents).toEqual([]);
}); });
it('should run blackListedEvents handler outside of ngZone', () => { it('should run blockListedEvents handler outside of ngZone', () => {
const Zone = (window as any)['Zone']; const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>'); const element = el('<div><div></div></div>');
doc.body.appendChild(element); doc.body.appendChild(element);
@ -311,45 +312,13 @@ import {createMouseEvent, el} from '../../../testing/src/browser_util';
let remover = manager.addEventListener(element, 'scroll', handler); let remover = manager.addEventListener(element, 'scroll', handler);
getDOM().dispatchEvent(element, dispatchedEvent); getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent); expect(receivedEvent).toBe(dispatchedEvent);
expect(receivedZone.name).not.toEqual('angular'); expect(receivedZone.name).toBe(Zone.root.name);
receivedEvent = null; receivedEvent = null;
remover && remover(); remover && remover();
getDOM().dispatchEvent(element, dispatchedEvent); getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null); expect(receivedEvent).toBe(null);
}); });
it('should only trigger one Change detection when bubbling', (done: DoneFn) => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc, zone, null);
const element = el('<div></div>');
const child = el('<div></div>');
element.appendChild(child);
doc.body.appendChild(element);
const dispatchedEvent = createMouseEvent('click');
let receivedEvents: any = [];
let stables: any = [];
const handler = (e: any) => { receivedEvents.push(e); };
const manager = new EventManager([domEventPlugin], zone);
let removerChild: any;
let removerParent: any;
zone.run(() => {
removerChild = manager.addEventListener(child, 'click', handler);
removerParent = manager.addEventListener(element, 'click', handler);
});
zone.onStable.subscribe((isStable: any) => { stables.push(isStable); });
getDOM().dispatchEvent(child, dispatchedEvent);
requestAnimationFrame(() => {
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);
removerChild && removerChild();
removerParent && removerParent();
done();
});
});
}); });
})(); })();
@ -363,12 +332,12 @@ class FakeEventManagerPlugin extends EventManagerPlugin {
addEventListener(element: any, eventName: string, handler: Function) { addEventListener(element: any, eventName: string, handler: Function) {
this.eventHandler[eventName] = handler; this.eventHandler[eventName] = handler;
return () => { delete this.eventHandler[eventName]; }; return () => { delete (this.eventHandler[eventName]); };
} }
} }
class FakeNgZone extends NgZone { class FakeNgZone extends NgZone {
constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: true}); } constructor() { super({enableLongStackTrace: false}); }
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return fn(); } run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { return fn(); }
runOutsideAngular(fn: Function) { return fn(); } runOutsideAngular(fn: Function) { return fn(); }
} }

View File

@ -175,7 +175,7 @@ export function stringifyElement(el: any /** TODO #9100 */): string {
} }
export function createNgZone(): NgZone { export function createNgZone(): NgZone {
return new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false}); return new NgZone({enableLongStackTrace: true});
} }
export function isCommentNode(node: Node): boolean { export function isCommentNode(node: Node): boolean {

View File

@ -624,17 +624,13 @@ export declare class NgProbeToken {
export declare class NgZone { export declare class NgZone {
readonly hasPendingMacrotasks: boolean; readonly hasPendingMacrotasks: boolean;
readonly hasPendingMicrotasks: boolean; readonly hasPendingMicrotasks: boolean;
readonly hasPendingZoneMicrotasks: boolean;
readonly isStable: boolean; readonly isStable: boolean;
readonly lastRequestAnimationFrameId: number;
readonly onError: EventEmitter<any>; readonly onError: EventEmitter<any>;
readonly onMicrotaskEmpty: EventEmitter<any>; readonly onMicrotaskEmpty: EventEmitter<any>;
readonly onStable: EventEmitter<any>; readonly onStable: EventEmitter<any>;
readonly onUnstable: EventEmitter<any>; readonly onUnstable: EventEmitter<any>;
readonly shouldCoalesceEventChangeDetection: boolean; constructor({ enableLongStackTrace }: {
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection }: {
enableLongStackTrace?: boolean | undefined; enableLongStackTrace?: boolean | undefined;
shouldCoalesceEventChangeDetection?: boolean | undefined;
}); });
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T; run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
runGuarded<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T; runGuarded<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;