feat(core): add shouldCoalesceRunChangeDetection option to coalesce change detections in the same event loop. (#39422)
Close #39348 Now `NgZone` has an option `shouldCoalesceEventChangeDetection` to coalesce multiple event handler's change detections to one async change detection. And there are some cases other than `event handler` have the same issues. In #39348, the case like this. ``` // This code results in one change detection occurring per // ngZone.run() call. This is entirely feasible, and can be a serious // performance issue. for (let i = 0; i < 100; i++) { this.ngZone.run(() => { // do something }); } ``` So such kind of case will trigger multiple change detections. And now with Ivy, we have a new `markDirty()` API will schedule a requestAnimationFrame to trigger change detection and also coalesce the change detections in the same event loop, `markDirty()` API doesn't only take care `event handler` but also all other cases `sync/macroTask/..` So this PR add a new option to coalesce change detections for all cases. test(core): add test case for shouldCoalesceEventChangeDetection option Add new test cases for current `shouldCoalesceEventChangeDetection` in `ng_zone.spec`, since currently we only have integration test for this one. PR Close #39422
This commit is contained in:
parent
d68cac69ed
commit
5e92d649f2
3
goldens/public-api/core/core.d.ts
vendored
3
goldens/public-api/core/core.d.ts
vendored
@ -619,9 +619,10 @@ export declare class NgZone {
|
|||||||
readonly onMicrotaskEmpty: EventEmitter<any>;
|
readonly onMicrotaskEmpty: EventEmitter<any>;
|
||||||
readonly onStable: EventEmitter<any>;
|
readonly onStable: EventEmitter<any>;
|
||||||
readonly onUnstable: EventEmitter<any>;
|
readonly onUnstable: EventEmitter<any>;
|
||||||
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection }: {
|
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection, shouldCoalesceRunChangeDetection }: {
|
||||||
enableLongStackTrace?: boolean | undefined;
|
enableLongStackTrace?: boolean | undefined;
|
||||||
shouldCoalesceEventChangeDetection?: boolean | undefined;
|
shouldCoalesceEventChangeDetection?: boolean | undefined;
|
||||||
|
shouldCoalesceRunChangeDetection?: 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;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 140899,
|
"main-es2015": 141516,
|
||||||
"polyfills-es2015": 36964
|
"polyfills-es2015": 36964
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2285,
|
"runtime-es2015": 2285,
|
||||||
"main-es2015": 241875,
|
"main-es2015": 242417,
|
||||||
"polyfills-es2015": 36709,
|
"polyfills-es2015": 36709,
|
||||||
"5-es2015": 745
|
"5-es2015": 745
|
||||||
}
|
}
|
||||||
@ -48,10 +48,10 @@
|
|||||||
"cli-hello-world-lazy-rollup": {
|
"cli-hello-world-lazy-rollup": {
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2285,
|
"runtime-es2015": 2289,
|
||||||
"main-es2015": 218340,
|
"main-es2015": 218507,
|
||||||
"polyfills-es2015": 36709,
|
"polyfills-es2015": 36723,
|
||||||
"5-es2015": 777
|
"5-es2015": 781
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -66,4 +66,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,6 +266,25 @@ export interface BootstrapOptions {
|
|||||||
* the change detection will only be triggered once.
|
* the change detection will only be triggered once.
|
||||||
*/
|
*/
|
||||||
ngZoneEventCoalescing?: boolean;
|
ngZoneEventCoalescing?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally specify if `NgZone#run()` method invocations should be coalesced
|
||||||
|
* into a single change detection.
|
||||||
|
*
|
||||||
|
* Consider the following case.
|
||||||
|
*
|
||||||
|
* for (let i = 0; i < 10; i ++) {
|
||||||
|
* ngZone.run(() => {
|
||||||
|
* // do something
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* This case triggers the change detection multiple times.
|
||||||
|
* With ngZoneRunCoalescing options, all change detections in an event loop trigger only once.
|
||||||
|
* In addition, the change detection executes in requestAnimation.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
ngZoneRunCoalescing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,10 +335,13 @@ export class PlatformRef {
|
|||||||
// 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 ngZoneEventCoalescing = (options && options.ngZoneEventCoalescing) || false;
|
||||||
const ngZone = getNgZone(ngZoneOption, ngZoneEventCoalescing);
|
const ngZoneRunCoalescing = (options && options.ngZoneRunCoalescing) || false;
|
||||||
|
const ngZone = getNgZone(ngZoneOption, {ngZoneEventCoalescing, ngZoneRunCoalescing});
|
||||||
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
|
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
|
||||||
// Attention: Don't use ApplicationRef.run here,
|
// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
|
||||||
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
|
// created within the Angular zone
|
||||||
|
// Do not try to replace ngZone.run with ApplicationRef#run because ApplicationRef would then be
|
||||||
|
// created outside of the Angular zone.
|
||||||
return ngZone.run(() => {
|
return ngZone.run(() => {
|
||||||
const ngZoneInjector = Injector.create(
|
const ngZoneInjector = Injector.create(
|
||||||
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
|
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
|
||||||
@ -426,7 +448,8 @@ export class PlatformRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNgZone(
|
function getNgZone(
|
||||||
ngZoneOption: NgZone|'zone.js'|'noop'|undefined, ngZoneEventCoalescing: boolean): NgZone {
|
ngZoneOption: NgZone|'zone.js'|'noop'|undefined,
|
||||||
|
extra?: {ngZoneEventCoalescing: boolean, ngZoneRunCoalescing: boolean}): NgZone {
|
||||||
let ngZone: NgZone;
|
let ngZone: NgZone;
|
||||||
|
|
||||||
if (ngZoneOption === 'noop') {
|
if (ngZoneOption === 'noop') {
|
||||||
@ -434,7 +457,8 @@ function getNgZone(
|
|||||||
} else {
|
} else {
|
||||||
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({
|
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({
|
||||||
enableLongStackTrace: isDevMode(),
|
enableLongStackTrace: isDevMode(),
|
||||||
shouldCoalesceEventChangeDetection: ngZoneEventCoalescing
|
shouldCoalesceEventChangeDetection: !!extra?.ngZoneEventCoalescing,
|
||||||
|
shouldCoalesceRunChangeDetection: !!extra?.ngZoneRunCoalescing
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return ngZone;
|
return ngZone;
|
||||||
|
@ -119,7 +119,11 @@ export class NgZone {
|
|||||||
readonly onError: EventEmitter<any> = new EventEmitter(false);
|
readonly onError: EventEmitter<any> = new EventEmitter(false);
|
||||||
|
|
||||||
|
|
||||||
constructor({enableLongStackTrace = false, shouldCoalesceEventChangeDetection = false}) {
|
constructor({
|
||||||
|
enableLongStackTrace = false,
|
||||||
|
shouldCoalesceEventChangeDetection = false,
|
||||||
|
shouldCoalesceRunChangeDetection = 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`);
|
||||||
}
|
}
|
||||||
@ -137,8 +141,11 @@ export class NgZone {
|
|||||||
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
|
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
|
||||||
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
|
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
|
||||||
}
|
}
|
||||||
|
// if shouldCoalesceRunChangeDetection is true, all tasks including event tasks will be
|
||||||
self.shouldCoalesceEventChangeDetection = shouldCoalesceEventChangeDetection;
|
// coalesced, so shouldCoalesceEventChangeDetection option is not necessary and can be skipped.
|
||||||
|
self.shouldCoalesceEventChangeDetection =
|
||||||
|
!shouldCoalesceRunChangeDetection && shouldCoalesceEventChangeDetection;
|
||||||
|
self.shouldCoalesceRunChangeDetection = shouldCoalesceRunChangeDetection;
|
||||||
self.lastRequestAnimationFrameId = -1;
|
self.lastRequestAnimationFrameId = -1;
|
||||||
self.nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
|
self.nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
|
||||||
forkInnerZoneWithAngularBehavior(self);
|
forkInnerZoneWithAngularBehavior(self);
|
||||||
@ -237,11 +244,49 @@ interface NgZonePrivate extends NgZone {
|
|||||||
hasPendingMicrotasks: boolean;
|
hasPendingMicrotasks: boolean;
|
||||||
lastRequestAnimationFrameId: number;
|
lastRequestAnimationFrameId: number;
|
||||||
isStable: boolean;
|
isStable: boolean;
|
||||||
|
/**
|
||||||
|
* 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 coalesce such kind of events to trigger
|
||||||
|
* change detection only once.
|
||||||
|
*
|
||||||
|
* By default, this option will be false. So the events will not be
|
||||||
|
* coalesced 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 it in an animation frame. So in the case above,
|
||||||
|
* the change detection will only be trigged once.
|
||||||
|
*/
|
||||||
shouldCoalesceEventChangeDetection: boolean;
|
shouldCoalesceEventChangeDetection: boolean;
|
||||||
|
/**
|
||||||
|
* Optionally specify if `NgZone#run()` method invocations should be coalesced
|
||||||
|
* into a single change detection.
|
||||||
|
*
|
||||||
|
* Consider the following case.
|
||||||
|
*
|
||||||
|
* for (let i = 0; i < 10; i ++) {
|
||||||
|
* ngZone.run(() => {
|
||||||
|
* // do something
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* This case triggers the change detection multiple times.
|
||||||
|
* With ngZoneRunCoalescing options, all change detections in an event loops trigger only once.
|
||||||
|
* In addition, the change detection executes in requestAnimation.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
shouldCoalesceRunChangeDetection: boolean;
|
||||||
|
|
||||||
nativeRequestAnimationFrame: (callback: FrameRequestCallback) => number;
|
nativeRequestAnimationFrame: (callback: FrameRequestCallback) => number;
|
||||||
|
|
||||||
// Cache of "fake" top eventTask. This is done so that we don't need to schedule a new task every
|
// Cache a "fake" top eventTask so you don't need to schedule a new task every
|
||||||
// time we want to run a `checkStable`.
|
// time you run a `checkStable`.
|
||||||
fakeTopEventTask: Task;
|
fakeTopEventTask: Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,12 +338,9 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||||||
const delayChangeDetectionForEventsDelegate = () => {
|
const delayChangeDetectionForEventsDelegate = () => {
|
||||||
delayChangeDetectionForEvents(zone);
|
delayChangeDetectionForEvents(zone);
|
||||||
};
|
};
|
||||||
const maybeDelayChangeDetection = !!zone.shouldCoalesceEventChangeDetection &&
|
|
||||||
zone.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:
|
onInvokeTask:
|
||||||
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
|
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
|
||||||
applyArgs: any): any => {
|
applyArgs: any): any => {
|
||||||
@ -306,14 +348,14 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||||||
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') {
|
if ((zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask') ||
|
||||||
maybeDelayChangeDetection();
|
zone.shouldCoalesceRunChangeDetection) {
|
||||||
|
delayChangeDetectionForEventsDelegate();
|
||||||
}
|
}
|
||||||
onLeave(zone);
|
onLeave(zone);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
onInvoke:
|
onInvoke:
|
||||||
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
|
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
|
||||||
applyArgs?: any[], source?: string): any => {
|
applyArgs?: any[], source?: string): any => {
|
||||||
@ -321,6 +363,9 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||||||
onEnter(zone);
|
onEnter(zone);
|
||||||
return delegate.invoke(target, callback, applyThis, applyArgs, source);
|
return delegate.invoke(target, callback, applyThis, applyArgs, source);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (zone.shouldCoalesceRunChangeDetection) {
|
||||||
|
delayChangeDetectionForEventsDelegate();
|
||||||
|
}
|
||||||
onLeave(zone);
|
onLeave(zone);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -351,7 +396,8 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||||||
|
|
||||||
function updateMicroTaskStatus(zone: NgZonePrivate) {
|
function updateMicroTaskStatus(zone: NgZonePrivate) {
|
||||||
if (zone._hasPendingMicrotasks ||
|
if (zone._hasPendingMicrotasks ||
|
||||||
(zone.shouldCoalesceEventChangeDetection && zone.lastRequestAnimationFrameId !== -1)) {
|
((zone.shouldCoalesceEventChangeDetection || zone.shouldCoalesceRunChangeDetection) &&
|
||||||
|
zone.lastRequestAnimationFrameId !== -1)) {
|
||||||
zone.hasPendingMicrotasks = true;
|
zone.hasPendingMicrotasks = true;
|
||||||
} else {
|
} else {
|
||||||
zone.hasPendingMicrotasks = false;
|
zone.hasPendingMicrotasks = false;
|
||||||
|
@ -12,6 +12,7 @@ import {AsyncTestCompleter, beforeEach, describe, expect, inject, it, Log, xit}
|
|||||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
|
|
||||||
import {scheduleMicroTask} from '../../src/util/microtask';
|
import {scheduleMicroTask} from '../../src/util/microtask';
|
||||||
|
import {getNativeRequestAnimationFrame} from '../../src/util/raf';
|
||||||
import {NoopNgZone} from '../../src/zone/ng_zone';
|
import {NoopNgZone} from '../../src/zone/ng_zone';
|
||||||
|
|
||||||
const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge;
|
const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge;
|
||||||
@ -929,4 +930,240 @@ function commonTests() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('coalescing', () => {
|
||||||
|
describe(
|
||||||
|
'shouldCoalesceRunChangeDetection = false, shouldCoalesceEventChangeDetection = false',
|
||||||
|
() => {
|
||||||
|
let notCoalesceZone: NgZone;
|
||||||
|
let logs: string[] = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
notCoalesceZone = new NgZone({});
|
||||||
|
logs = [];
|
||||||
|
notCoalesceZone.onMicrotaskEmpty.subscribe(() => {
|
||||||
|
logs.push('microTask empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run sync', () => {
|
||||||
|
notCoalesceZone.run(() => {});
|
||||||
|
expect(logs).toEqual(['microTask empty']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit onMicroTaskEmpty multiple times within the same event loop for multiple ngZone.run',
|
||||||
|
() => {
|
||||||
|
notCoalesceZone.run(() => {});
|
||||||
|
notCoalesceZone.run(() => {});
|
||||||
|
expect(logs).toEqual(['microTask empty', 'microTask empty']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit onMicroTaskEmpty multiple times within the same event loop for multiple tasks',
|
||||||
|
() => {
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
notCoalesceZone.run(() => {
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask1');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask2');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleMacroTask('myMacro', () => {
|
||||||
|
logs.push('macroTask');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
});
|
||||||
|
tasks.forEach(t => t.invoke());
|
||||||
|
expect(logs).toEqual([
|
||||||
|
'microTask empty', 'eventTask1', 'microTask empty', 'eventTask2',
|
||||||
|
'microTask empty', 'macroTask', 'microTask empty'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldCoalesceEventChangeDetection = true, shouldCoalesceRunChangeDetection = false', () => {
|
||||||
|
let nativeRequestAnimationFrame: (fn: FrameRequestCallback) => void;
|
||||||
|
if (!(global as any).requestAnimationFrame) {
|
||||||
|
nativeRequestAnimationFrame = function(fn: Function) {
|
||||||
|
(global as any)[Zone.__symbol__('setTimeout')](fn, 16);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
let patchedImmediate: any;
|
||||||
|
let coalesceZone: NgZone;
|
||||||
|
let logs: string[] = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
patchedImmediate = setImmediate;
|
||||||
|
(global as any).setImmediate = (global as any)[Zone.__symbol__('setImmediate')];
|
||||||
|
coalesceZone = new NgZone({shouldCoalesceEventChangeDetection: true});
|
||||||
|
logs = [];
|
||||||
|
coalesceZone.onMicrotaskEmpty.subscribe(() => {
|
||||||
|
logs.push('microTask empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
(global as any).setImmediate = patchedImmediate;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run in requestAnimationFrame async', (done: DoneFn) => {
|
||||||
|
let task: Task|undefined = undefined;
|
||||||
|
coalesceZone.run(() => {
|
||||||
|
task = Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('myEvent');
|
||||||
|
}, undefined, () => {});
|
||||||
|
});
|
||||||
|
task!.invoke();
|
||||||
|
expect(logs).toEqual(['microTask empty', 'myEvent']);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(['microTask empty', 'myEvent', 'microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only emit onMicroTaskEmpty once within the same event loop for multiple event tasks',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
coalesceZone.run(() => {
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask1');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask2');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
});
|
||||||
|
tasks.forEach(t => t.invoke());
|
||||||
|
expect(logs).toEqual(['microTask empty', 'eventTask1', 'eventTask2']);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(
|
||||||
|
['microTask empty', 'eventTask1', 'eventTask2', 'microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit onMicroTaskEmpty once within the same event loop for not only event tasks, but event tasks are before other tasks',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
coalesceZone.run(() => {
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask1');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask2');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleMacroTask('myMacro', () => {
|
||||||
|
logs.push('macroTask');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
});
|
||||||
|
tasks.forEach(t => t.invoke());
|
||||||
|
expect(logs).toEqual(['microTask empty', 'eventTask1', 'eventTask2', 'macroTask']);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(
|
||||||
|
['microTask empty', 'eventTask1', 'eventTask2', 'macroTask', 'microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit multiple onMicroTaskEmpty within the same event loop for not only event tasks, but event tasks are after other tasks',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
coalesceZone.run(() => {
|
||||||
|
tasks.push(Zone.current.scheduleMacroTask('myMacro', () => {
|
||||||
|
logs.push('macroTask');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask1');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask2');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
});
|
||||||
|
tasks.forEach(t => t.invoke());
|
||||||
|
expect(logs).toEqual(
|
||||||
|
['microTask empty', 'macroTask', 'microTask empty', 'eventTask1', 'eventTask2']);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual([
|
||||||
|
'microTask empty', 'macroTask', 'microTask empty', 'eventTask1', 'eventTask2',
|
||||||
|
'microTask empty'
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldCoalesceRunChangeDetection = true', () => {
|
||||||
|
let nativeRequestAnimationFrame: (fn: FrameRequestCallback) => void;
|
||||||
|
if (!(global as any).requestAnimationFrame) {
|
||||||
|
nativeRequestAnimationFrame = function(fn: Function) {
|
||||||
|
(global as any)[Zone.__symbol__('setTimeout')](fn, 16);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
let patchedImmediate: any;
|
||||||
|
let coalesceZone: NgZone;
|
||||||
|
let logs: string[] = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
patchedImmediate = setImmediate;
|
||||||
|
(global as any).setImmediate = (global as any)[Zone.__symbol__('setImmediate')];
|
||||||
|
coalesceZone = new NgZone({shouldCoalesceRunChangeDetection: true});
|
||||||
|
logs = [];
|
||||||
|
coalesceZone.onMicrotaskEmpty.subscribe(() => {
|
||||||
|
logs.push('microTask empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
(global as any).setImmediate = patchedImmediate;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run in requestAnimationFrame async', (done: DoneFn) => {
|
||||||
|
coalesceZone.run(() => {});
|
||||||
|
expect(logs).toEqual([]);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(['microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only emit onMicroTaskEmpty once within the same event loop for multiple ngZone.run',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
coalesceZone.run(() => {});
|
||||||
|
coalesceZone.run(() => {});
|
||||||
|
expect(logs).toEqual([]);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(['microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only emit onMicroTaskEmpty once within the same event loop for multiple tasks',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
coalesceZone.run(() => {
|
||||||
|
tasks.push(Zone.current.scheduleMacroTask('myMacro', () => {
|
||||||
|
logs.push('macroTask');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask1');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleEventTask('myEvent', () => {
|
||||||
|
logs.push('eventTask2');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
tasks.push(Zone.current.scheduleMacroTask('myMacro', () => {
|
||||||
|
logs.push('macroTask');
|
||||||
|
}, undefined, () => {}));
|
||||||
|
});
|
||||||
|
tasks.forEach(t => t.invoke());
|
||||||
|
expect(logs).toEqual(['macroTask', 'eventTask1', 'eventTask2', 'macroTask']);
|
||||||
|
nativeRequestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(
|
||||||
|
['macroTask', 'eventTask1', 'eventTask2', 'macroTask', 'microTask empty']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -335,41 +335,79 @@ describe('EventManager', () => {
|
|||||||
expect(receivedEvent).toBe(null);
|
expect(receivedEvent).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only trigger one Change detection when bubbling', (done: DoneFn) => {
|
it('should only trigger one Change detection when bubbling with shouldCoalesceEventChangeDetection = true',
|
||||||
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
|
(done: DoneFn) => {
|
||||||
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
|
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
|
||||||
domEventPlugin = new DomEventsPlugin(doc);
|
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
|
||||||
const element = el('<div></div>');
|
domEventPlugin = new DomEventsPlugin(doc);
|
||||||
const child = el('<div></div>');
|
const element = el('<div></div>');
|
||||||
element.appendChild(child);
|
const child = el('<div></div>');
|
||||||
doc.body.appendChild(element);
|
element.appendChild(child);
|
||||||
const dispatchedEvent = createMouseEvent('click');
|
doc.body.appendChild(element);
|
||||||
let receivedEvents: any = [];
|
const dispatchedEvent = createMouseEvent('click');
|
||||||
let stables: any = [];
|
let receivedEvents: any = [];
|
||||||
const handler = (e: any) => {
|
let stables: any = [];
|
||||||
receivedEvents.push(e);
|
const handler = (e: any) => {
|
||||||
};
|
receivedEvents.push(e);
|
||||||
const manager = new EventManager([domEventPlugin], zone);
|
};
|
||||||
let removerChild: any;
|
const manager = new EventManager([domEventPlugin], zone);
|
||||||
let removerParent: any;
|
let removerChild: any;
|
||||||
|
let removerParent: any;
|
||||||
|
|
||||||
zone.run(() => {
|
zone.run(() => {
|
||||||
removerChild = manager.addEventListener(child, 'click', handler);
|
removerChild = manager.addEventListener(child, 'click', handler);
|
||||||
removerParent = manager.addEventListener(element, 'click', handler);
|
removerParent = manager.addEventListener(element, 'click', handler);
|
||||||
});
|
});
|
||||||
zone.onStable.subscribe((isStable: any) => {
|
zone.onStable.subscribe((isStable: any) => {
|
||||||
stables.push(isStable);
|
stables.push(isStable);
|
||||||
});
|
});
|
||||||
getDOM().dispatchEvent(child, dispatchedEvent);
|
getDOM().dispatchEvent(child, dispatchedEvent);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
expect(receivedEvents.length).toBe(2);
|
expect(receivedEvents.length).toBe(2);
|
||||||
expect(stables.length).toBe(1);
|
expect(stables.length).toBe(1);
|
||||||
|
|
||||||
removerChild && removerChild();
|
removerChild && removerChild();
|
||||||
removerParent && removerParent();
|
removerParent && removerParent();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only trigger one Change detection when bubbling with shouldCoalesceRunChangeDetection = true',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
|
||||||
|
zone = new NgZone({shouldCoalesceRunChangeDetection: true});
|
||||||
|
domEventPlugin = new DomEventsPlugin(doc);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not drain micro tasks queue too early with shouldCoalesceEventChangeDetection=true',
|
it('should not drain micro tasks queue too early with shouldCoalesceEventChangeDetection=true',
|
||||||
(done: DoneFn) => {
|
(done: DoneFn) => {
|
||||||
@ -393,6 +431,52 @@ describe('EventManager', () => {
|
|||||||
let removerParent: any;
|
let removerParent: any;
|
||||||
let removerChildFocus: any;
|
let removerChildFocus: any;
|
||||||
|
|
||||||
|
zone.run(() => {
|
||||||
|
removerParent = manager.addEventListener(element, 'click', handler);
|
||||||
|
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
|
||||||
|
});
|
||||||
|
const sub = zone.onStable.subscribe(() => {
|
||||||
|
logs.push('begin');
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
logs.push('promise resolved');
|
||||||
|
});
|
||||||
|
element.appendChild(child);
|
||||||
|
getDOM().dispatchEvent(child, dispatchedBlurEvent);
|
||||||
|
sub.unsubscribe();
|
||||||
|
logs.push('end');
|
||||||
|
});
|
||||||
|
getDOM().dispatchEvent(element, dispatchedClickEvent);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
expect(logs).toEqual(['begin', 'blur', 'end', 'promise resolved']);
|
||||||
|
|
||||||
|
removerParent && removerParent();
|
||||||
|
removerChildFocus && removerChildFocus();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not drain micro tasks queue too early with shouldCoalesceRunChangeDetection=true',
|
||||||
|
(done: DoneFn) => {
|
||||||
|
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
|
||||||
|
zone = new NgZone({shouldCoalesceRunChangeDetection: true});
|
||||||
|
domEventPlugin = new DomEventsPlugin(doc);
|
||||||
|
const element = el('<div></div>');
|
||||||
|
const child = el('<div></div>');
|
||||||
|
doc.body.appendChild(element);
|
||||||
|
const dispatchedClickEvent = createMouseEvent('click');
|
||||||
|
const dispatchedBlurEvent: FocusEvent =
|
||||||
|
getDOM().getDefaultDocument().createEvent('FocusEvent');
|
||||||
|
dispatchedBlurEvent.initEvent('blur', true, true);
|
||||||
|
let logs: any = [];
|
||||||
|
const handler = () => {};
|
||||||
|
|
||||||
|
const blurHandler = (e: any) => {
|
||||||
|
logs.push('blur');
|
||||||
|
};
|
||||||
|
const manager = new EventManager([domEventPlugin], zone);
|
||||||
|
let removerParent: any;
|
||||||
|
let removerChildFocus: any;
|
||||||
|
|
||||||
zone.run(() => {
|
zone.run(() => {
|
||||||
removerParent = manager.addEventListener(element, 'click', handler);
|
removerParent = manager.addEventListener(element, 'click', handler);
|
||||||
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
|
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user