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
|
@ -619,9 +619,10 @@ export declare class NgZone {
|
|||
readonly onMicrotaskEmpty: EventEmitter<any>;
|
||||
readonly onStable: EventEmitter<any>;
|
||||
readonly onUnstable: EventEmitter<any>;
|
||||
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection }: {
|
||||
constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection, shouldCoalesceRunChangeDetection }: {
|
||||
enableLongStackTrace?: boolean | undefined;
|
||||
shouldCoalesceEventChangeDetection?: boolean | undefined;
|
||||
shouldCoalesceRunChangeDetection?: boolean | undefined;
|
||||
});
|
||||
run<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": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 1485,
|
||||
"main-es2015": 140899,
|
||||
"main-es2015": 141516,
|
||||
"polyfills-es2015": 36964
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2285,
|
||||
"main-es2015": 241875,
|
||||
"main-es2015": 242417,
|
||||
"polyfills-es2015": 36709,
|
||||
"5-es2015": 745
|
||||
}
|
||||
|
@ -48,10 +48,10 @@
|
|||
"cli-hello-world-lazy-rollup": {
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2285,
|
||||
"main-es2015": 218340,
|
||||
"polyfills-es2015": 36709,
|
||||
"5-es2015": 777
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 218507,
|
||||
"polyfills-es2015": 36723,
|
||||
"5-es2015": 781
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -266,6 +266,25 @@ export interface BootstrapOptions {
|
|||
* the change detection will only be triggered once.
|
||||
*/
|
||||
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.
|
||||
const ngZoneOption = options ? options.ngZone : undefined;
|
||||
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}];
|
||||
// Attention: Don't use ApplicationRef.run here,
|
||||
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
|
||||
// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
|
||||
// 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(() => {
|
||||
const ngZoneInjector = Injector.create(
|
||||
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
|
||||
|
@ -426,7 +448,8 @@ export class PlatformRef {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
if (ngZoneOption === 'noop') {
|
||||
|
@ -434,7 +457,8 @@ function getNgZone(
|
|||
} else {
|
||||
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({
|
||||
enableLongStackTrace: isDevMode(),
|
||||
shouldCoalesceEventChangeDetection: ngZoneEventCoalescing
|
||||
shouldCoalesceEventChangeDetection: !!extra?.ngZoneEventCoalescing,
|
||||
shouldCoalesceRunChangeDetection: !!extra?.ngZoneRunCoalescing
|
||||
});
|
||||
}
|
||||
return ngZone;
|
||||
|
|
|
@ -119,7 +119,11 @@ export class NgZone {
|
|||
readonly onError: EventEmitter<any> = new EventEmitter(false);
|
||||
|
||||
|
||||
constructor({enableLongStackTrace = false, shouldCoalesceEventChangeDetection = false}) {
|
||||
constructor({
|
||||
enableLongStackTrace = false,
|
||||
shouldCoalesceEventChangeDetection = false,
|
||||
shouldCoalesceRunChangeDetection = false
|
||||
}) {
|
||||
if (typeof Zone == 'undefined') {
|
||||
throw new Error(`In this configuration Angular requires Zone.js`);
|
||||
}
|
||||
|
@ -137,8 +141,11 @@ export class NgZone {
|
|||
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
|
||||
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
|
||||
}
|
||||
|
||||
self.shouldCoalesceEventChangeDetection = shouldCoalesceEventChangeDetection;
|
||||
// if shouldCoalesceRunChangeDetection is true, all tasks including event tasks will be
|
||||
// coalesced, so shouldCoalesceEventChangeDetection option is not necessary and can be skipped.
|
||||
self.shouldCoalesceEventChangeDetection =
|
||||
!shouldCoalesceRunChangeDetection && shouldCoalesceEventChangeDetection;
|
||||
self.shouldCoalesceRunChangeDetection = shouldCoalesceRunChangeDetection;
|
||||
self.lastRequestAnimationFrameId = -1;
|
||||
self.nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame;
|
||||
forkInnerZoneWithAngularBehavior(self);
|
||||
|
@ -237,11 +244,49 @@ interface NgZonePrivate extends NgZone {
|
|||
hasPendingMicrotasks: boolean;
|
||||
lastRequestAnimationFrameId: number;
|
||||
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;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Cache of "fake" top eventTask. This is done so that we don't need to schedule a new task every
|
||||
// time we want to run a `checkStable`.
|
||||
// Cache a "fake" top eventTask so you don't need to schedule a new task every
|
||||
// time you run a `checkStable`.
|
||||
fakeTopEventTask: Task;
|
||||
}
|
||||
|
||||
|
@ -293,12 +338,9 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||
const delayChangeDetectionForEventsDelegate = () => {
|
||||
delayChangeDetectionForEvents(zone);
|
||||
};
|
||||
const maybeDelayChangeDetection = !!zone.shouldCoalesceEventChangeDetection &&
|
||||
zone.nativeRequestAnimationFrame && delayChangeDetectionForEventsDelegate;
|
||||
zone._inner = zone._inner.fork({
|
||||
name: 'angular',
|
||||
properties:
|
||||
<any>{'isAngularZone': true, 'maybeDelayChangeDetection': maybeDelayChangeDetection},
|
||||
properties: <any>{'isAngularZone': true},
|
||||
onInvokeTask:
|
||||
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
|
||||
applyArgs: any): any => {
|
||||
|
@ -306,14 +348,14 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||
onEnter(zone);
|
||||
return delegate.invokeTask(target, task, applyThis, applyArgs);
|
||||
} finally {
|
||||
if (maybeDelayChangeDetection && task.type === 'eventTask') {
|
||||
maybeDelayChangeDetection();
|
||||
if ((zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask') ||
|
||||
zone.shouldCoalesceRunChangeDetection) {
|
||||
delayChangeDetectionForEventsDelegate();
|
||||
}
|
||||
onLeave(zone);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onInvoke:
|
||||
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
|
||||
applyArgs?: any[], source?: string): any => {
|
||||
|
@ -321,6 +363,9 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||
onEnter(zone);
|
||||
return delegate.invoke(target, callback, applyThis, applyArgs, source);
|
||||
} finally {
|
||||
if (zone.shouldCoalesceRunChangeDetection) {
|
||||
delayChangeDetectionForEventsDelegate();
|
||||
}
|
||||
onLeave(zone);
|
||||
}
|
||||
},
|
||||
|
@ -351,7 +396,8 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
|||
|
||||
function updateMicroTaskStatus(zone: NgZonePrivate) {
|
||||
if (zone._hasPendingMicrotasks ||
|
||||
(zone.shouldCoalesceEventChangeDetection && zone.lastRequestAnimationFrameId !== -1)) {
|
||||
((zone.shouldCoalesceEventChangeDetection || zone.shouldCoalesceRunChangeDetection) &&
|
||||
zone.lastRequestAnimationFrameId !== -1)) {
|
||||
zone.hasPendingMicrotasks = true;
|
||||
} else {
|
||||
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 {scheduleMicroTask} from '../../src/util/microtask';
|
||||
import {getNativeRequestAnimationFrame} from '../../src/util/raf';
|
||||
import {NoopNgZone} from '../../src/zone/ng_zone';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
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;
|
||||
it('should only trigger one Change detection when bubbling with shouldCoalesceEventChangeDetection = true',
|
||||
(done: DoneFn) => {
|
||||
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
|
||||
zone = new NgZone({shouldCoalesceEventChangeDetection: 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);
|
||||
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();
|
||||
});
|
||||
});
|
||||
removerChild && removerChild();
|
||||
removerParent && removerParent();
|
||||
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',
|
||||
(done: DoneFn) => {
|
||||
|
@ -393,6 +431,52 @@ describe('EventManager', () => {
|
|||
let removerParent: 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(() => {
|
||||
removerParent = manager.addEventListener(element, 'click', handler);
|
||||
removerChildFocus = manager.addEventListener(child, 'blur', blurHandler);
|
||||
|
|
Loading…
Reference in New Issue