feat(core): add isStable Observable property to ApplicationRef to indicate when it's stable and unstable (#14337)
PR Close #14337
This commit is contained in:

committed by
Miško Hevery

parent
b64946b5f9
commit
c4817988ca
@ -6,9 +6,16 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
import {Observer} from 'rxjs/Observer';
|
||||||
|
import {Subject} from 'rxjs/Subject';
|
||||||
|
import {Subscription} from 'rxjs/Subscription';
|
||||||
|
import {merge} from 'rxjs/observable/merge';
|
||||||
|
import {share} from 'rxjs/operator/share';
|
||||||
|
|
||||||
import {ErrorHandler} from '../src/error_handler';
|
import {ErrorHandler} from '../src/error_handler';
|
||||||
import {ListWrapper} from '../src/facade/collection';
|
import {ListWrapper} from '../src/facade/collection';
|
||||||
import {stringify} from '../src/facade/lang';
|
import {scheduleMicroTask, stringify} from '../src/facade/lang';
|
||||||
import {isPromise} from '../src/util/lang';
|
import {isPromise} from '../src/util/lang';
|
||||||
|
|
||||||
import {ApplicationInitStatus} from './application_init';
|
import {ApplicationInitStatus} from './application_init';
|
||||||
@ -395,6 +402,11 @@ export abstract class ApplicationRef {
|
|||||||
* Returns the number of attached views.
|
* Returns the number of attached views.
|
||||||
*/
|
*/
|
||||||
abstract get viewCount(): number;
|
abstract get viewCount(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Observable that indicates when the application is stable or unstable.
|
||||||
|
*/
|
||||||
|
abstract get isStable(): Observable<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -412,6 +424,8 @@ export class ApplicationRef_ extends ApplicationRef {
|
|||||||
private _views: AppView<any>[] = [];
|
private _views: AppView<any>[] = [];
|
||||||
private _runningTick: boolean = false;
|
private _runningTick: boolean = false;
|
||||||
private _enforceNoNewChanges: boolean = false;
|
private _enforceNoNewChanges: boolean = false;
|
||||||
|
private _isStable: Observable<boolean>;
|
||||||
|
private _stable = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _zone: NgZone, private _console: Console, private _injector: Injector,
|
private _zone: NgZone, private _console: Console, private _injector: Injector,
|
||||||
@ -425,6 +439,46 @@ export class ApplicationRef_ extends ApplicationRef {
|
|||||||
|
|
||||||
this._zone.onMicrotaskEmpty.subscribe(
|
this._zone.onMicrotaskEmpty.subscribe(
|
||||||
{next: () => { this._zone.run(() => { this.tick(); }); }});
|
{next: () => { this._zone.run(() => { this.tick(); }); }});
|
||||||
|
|
||||||
|
const isCurrentlyStable = new Observable<boolean>((observer: Observer<boolean>) => {
|
||||||
|
this._stable = this._zone.isStable && !this._zone.hasPendingMacrotasks &&
|
||||||
|
!this._zone.hasPendingMicrotasks;
|
||||||
|
this._zone.runOutsideAngular(() => {
|
||||||
|
observer.next(this._stable);
|
||||||
|
observer.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isStable = new Observable<boolean>((observer: Observer<boolean>) => {
|
||||||
|
const stableSub: Subscription = this._zone.onStable.subscribe(() => {
|
||||||
|
NgZone.assertNotInAngularZone();
|
||||||
|
|
||||||
|
// Check whether there are no pending macro/micro tasks in the next tick
|
||||||
|
// to allow for NgZone to update the state.
|
||||||
|
scheduleMicroTask(() => {
|
||||||
|
if (!this._stable && !this._zone.hasPendingMacrotasks &&
|
||||||
|
!this._zone.hasPendingMicrotasks) {
|
||||||
|
this._stable = true;
|
||||||
|
observer.next(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const unstableSub: Subscription = this._zone.onUnstable.subscribe(() => {
|
||||||
|
NgZone.assertInAngularZone();
|
||||||
|
if (this._stable) {
|
||||||
|
this._stable = false;
|
||||||
|
this._zone.runOutsideAngular(() => { observer.next(false); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stableSub.unsubscribe();
|
||||||
|
unstableSub.unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this._isStable = merge(isCurrentlyStable, share.call(isStable));
|
||||||
}
|
}
|
||||||
|
|
||||||
attachView(viewRef: ViewRef): void {
|
attachView(viewRef: ViewRef): void {
|
||||||
@ -510,4 +564,6 @@ export class ApplicationRef_ extends ApplicationRef {
|
|||||||
get componentTypes(): Type<any>[] { return this._rootComponentTypes; }
|
get componentTypes(): Type<any>[] { return this._rootComponentTypes; }
|
||||||
|
|
||||||
get components(): ComponentRef<any>[] { return this._rootComponents; }
|
get components(): ComponentRef<any>[] { return this._rootComponents; }
|
||||||
|
|
||||||
|
get isStable(): Observable<boolean> { return this._isStable; }
|
||||||
}
|
}
|
||||||
|
@ -6,17 +6,18 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||||
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
|
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
|
||||||
import {ErrorHandler} from '@angular/core/src/error_handler';
|
import {ErrorHandler} from '@angular/core/src/error_handler';
|
||||||
import {ComponentRef} from '@angular/core/src/linker/component_factory';
|
import {ComponentRef} from '@angular/core/src/linker/component_factory';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
|
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
|
||||||
|
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
|
||||||
import {expect} from '@angular/platform-browser/testing/matchers';
|
import {expect} from '@angular/platform-browser/testing/matchers';
|
||||||
import {ServerModule} from '@angular/platform-server';
|
import {ServerModule} from '@angular/platform-server';
|
||||||
|
|
||||||
import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
|
import {ComponentFixture, ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
|
||||||
|
|
||||||
@Component({selector: 'comp', template: 'hello'})
|
@Component({selector: 'comp', template: 'hello'})
|
||||||
class SomeComponent {
|
class SomeComponent {
|
||||||
@ -123,7 +124,6 @@ export function main() {
|
|||||||
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
|
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('bootstrapModule', () => {
|
describe('bootstrapModule', () => {
|
||||||
@ -363,10 +363,143 @@ export function main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
@Component({selector: 'my-comp', template: ''})
|
describe('AppRef', () => {
|
||||||
class MyComp6 {
|
@Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
|
||||||
|
class SyncComp {
|
||||||
|
text: string = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'click-comp', template: `<span (click)="onClick()">{{text}}</span>`})
|
||||||
|
class ClickComp {
|
||||||
|
text: string = '1';
|
||||||
|
|
||||||
|
onClick() { this.text += '1'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'micro-task-comp', template: `<span>{{text}}</span>`})
|
||||||
|
class MicroTaskComp {
|
||||||
|
text: string = '1';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
Promise.resolve(null).then((_) => { this.text += '1'; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'macro-task-comp', template: `<span>{{text}}</span>`})
|
||||||
|
class MacroTaskComp {
|
||||||
|
text: string = '1';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
setTimeout(() => { this.text += '1'; }, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'micro-macro-task-comp', template: `<span>{{text}}</span>`})
|
||||||
|
class MicroMacroTaskComp {
|
||||||
|
text: string = '1';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
Promise.resolve(null).then((_) => {
|
||||||
|
this.text += '1';
|
||||||
|
setTimeout(() => { this.text += '1'; }, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'macro-micro-task-comp', template: `<span>{{text}}</span>`})
|
||||||
|
class MacroMicroTaskComp {
|
||||||
|
text: string = '1';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.text += '1';
|
||||||
|
Promise.resolve(null).then((_: any) => { this.text += '1'; });
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stableCalled = false;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stableCalled = false;
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
SyncComp, MicroTaskComp, MacroTaskComp, MicroMacroTaskComp, MacroMicroTaskComp, ClickComp
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => { expect(stableCalled).toBe(true, 'isStable did not emit true on stable'); });
|
||||||
|
|
||||||
|
function expectStableTexts(component: Type<any>, expected: string[]) {
|
||||||
|
const fixture = TestBed.createComponent(component);
|
||||||
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
||||||
|
const zone: NgZone = TestBed.get(NgZone);
|
||||||
|
appRef.attachView(fixture.componentRef.hostView);
|
||||||
|
zone.run(() => appRef.tick());
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
appRef.isStable.subscribe({
|
||||||
|
next: (stable: boolean) => {
|
||||||
|
if (stable) {
|
||||||
|
expect(i).toBeLessThan(expected.length);
|
||||||
|
expect(fixture.nativeElement).toHaveText(expected[i++]);
|
||||||
|
stableCalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('isStable should fire on synchronous component loading',
|
||||||
|
async(() => { expectStableTexts(SyncComp, ['1']); }));
|
||||||
|
|
||||||
|
it('isStable should fire after a microtask on init is completed',
|
||||||
|
async(() => { expectStableTexts(MicroTaskComp, ['11']); }));
|
||||||
|
|
||||||
|
it('isStable should fire after a macrotask on init is completed',
|
||||||
|
async(() => { expectStableTexts(MacroTaskComp, ['11']); }));
|
||||||
|
|
||||||
|
it('isStable should fire only after chain of micro and macrotasks on init are completed',
|
||||||
|
async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); }));
|
||||||
|
|
||||||
|
it('isStable should fire only after chain of macro and microtasks on init are completed',
|
||||||
|
async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); }));
|
||||||
|
|
||||||
|
describe('unstable', () => {
|
||||||
|
let unstableCalled = false;
|
||||||
|
|
||||||
|
afterEach(
|
||||||
|
() => { expect(unstableCalled).toBe(true, 'isStable did not emit false on unstable'); });
|
||||||
|
|
||||||
|
function expectUnstable(appRef: ApplicationRef) {
|
||||||
|
appRef.isStable.subscribe({
|
||||||
|
next: (stable: boolean) => {
|
||||||
|
if (stable) {
|
||||||
|
stableCalled = true;
|
||||||
|
}
|
||||||
|
if (!stable) {
|
||||||
|
unstableCalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should be fired after app becomes unstable', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(ClickComp);
|
||||||
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
||||||
|
const zone: NgZone = TestBed.get(NgZone);
|
||||||
|
appRef.attachView(fixture.componentRef.hostView);
|
||||||
|
zone.run(() => appRef.tick());
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expectUnstable(appRef);
|
||||||
|
const element = fixture.debugElement.children[0];
|
||||||
|
dispatchEvent(element.nativeElement, 'click');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockConsole {
|
class MockConsole {
|
||||||
|
1
tools/public_api_guard/core/index.d.ts
vendored
1
tools/public_api_guard/core/index.d.ts
vendored
@ -171,6 +171,7 @@ export declare class ApplicationModule {
|
|||||||
export declare abstract class ApplicationRef {
|
export declare abstract class ApplicationRef {
|
||||||
readonly abstract componentTypes: Type<any>[];
|
readonly abstract componentTypes: Type<any>[];
|
||||||
readonly abstract components: ComponentRef<any>[];
|
readonly abstract components: ComponentRef<any>[];
|
||||||
|
readonly abstract isStable: Observable<boolean>;
|
||||||
readonly abstract viewCount: number;
|
readonly abstract viewCount: number;
|
||||||
abstract attachView(view: ViewRef): void;
|
abstract attachView(view: ViewRef): void;
|
||||||
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
|
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
|
||||||
|
Reference in New Issue
Block a user