perf(ivy): Improve performance of transplanted views (#33702)

PR Close #33702
This commit is contained in:
Miško Hevery
2019-11-08 15:13:22 -08:00
committed by Alex Rickabaugh
parent 1860a9edbc
commit a16a57e52a
12 changed files with 307 additions and 94 deletions

View File

@ -9,12 +9,10 @@
import {CommonModule} from '@angular/common';
import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, Input, NgModule, OnInit, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {markDirty} from '@angular/core/src/render3/index';
import {CONTEXT} from '@angular/core/src/render3/interfaces/view';
import {getCheckNoChangesMode, instructionState} from '@angular/core/src/render3/state';
import {AfterContentChecked, AfterViewChecked} from '@angular/core/src/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
describe('change detection', () => {
@ -1108,16 +1106,22 @@ describe('change detection', () => {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
InsertComp({{greeting}})
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{$implicit: greeting}">
</ng-container>
<div *ngIf="true">
<!-- Add extra level of embedded view to ensure we can handle nesting -->
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{$implicit: greeting}">
</ng-container>
</div>
`
})
class InsertComp {
class InsertComp implements DoCheck,
AfterViewChecked {
get template(): TemplateRef<any> { return declareComp.myTmpl; }
greeting: string = 'Hello';
constructor(public changeDetectorRef: ChangeDetectorRef) { insertComp = this; }
ngDoCheck(): void { logValue = 'Insert'; }
ngAfterViewChecked(): void { logValue = null; }
}
@Component({
@ -1125,30 +1129,24 @@ describe('change detection', () => {
template: `
DeclareComp({{name}})
<ng-template #myTmpl let-greeting>
{{greeting}} {{nameWithLog}}!
{{greeting}} {{logName()}}!
</ng-template>
`
})
class DeclareComp {
class DeclareComp implements DoCheck,
AfterViewChecked {
@ViewChild('myTmpl')
myTmpl !: TemplateRef<any>;
name: string = 'world';
get nameWithLog() {
if (ivyEnabled && !getCheckNoChangesMode()) {
const lFrame = instructionState.lFrame;
const parentChangeDetectionLView = lFrame.parent.lView;
if (parentChangeDetectionLView[CONTEXT] === declareComp) {
log.push('Declare');
} else if (parentChangeDetectionLView[CONTEXT] === insertComp) {
log.push('Insert');
} else {
log.push(
'UNEXPECTED VIEW: ' + parentChangeDetectionLView ![CONTEXT] !.constructor.name);
}
}
constructor() { declareComp = this; }
ngDoCheck(): void { logValue = 'Declare'; }
logName() {
// This will log when the embedded view gets CD. The `logValue` will show if the CD was from
// `Insert` or from `Declare` component.
log.push(logValue !);
return this.name;
}
constructor() { declareComp = this; }
ngAfterViewChecked(): void { logValue = null; }
}
@Component({
@ -1164,6 +1162,7 @@ describe('change detection', () => {
}
let log !: string[];
let logValue !: string | null;
let fixture !: ComponentFixture<AppComp>;
let appComp !: AppComp;
let insertComp !: InsertComp;
@ -1179,31 +1178,46 @@ describe('change detection', () => {
});
it('should CD with declaration', () => {
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Insert']);
// NOTE: The CD of VE and Ivy is different and is captured in the assertions:
// `expect(log).toEqual(ivyEnabled ? [...] : [...])`
//
// The reason for this difference is in the algorithm which VE and Ivy use to deal with
// transplanted views:
// - VE: always runs CD at insertion point. If the insertion component is `OnPush` and the
// transplanted view is `CheckAlways` then the insertion component will be changed to
// `CheckAlways` (defeating the benefit of `OnPush`)
// - Ivy: Runs the CD at both the declaration as well as insertion point. The benefit of this
// approach is that each side (declaration/insertion) gets to keep its own semantics (either
// `OnPush` or `CheckAlways`). The implication is that:
// 1. The two semantics are slightly different.
// 2. Ivy will CD the transplanted view twice under some circumstances. (When both insertion
// and declaration are both dirty.)
fixture.detectChanges(false);
expect(log).toEqual(['Insert']);
log.length = 0;
expect(trim(fixture.nativeElement.textContent))
.toEqual('DeclareComp(world) InsertComp(Hello) Hello world!');
declareComp.name = 'Angular';
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Declare']);
fixture.detectChanges(false);
expect(log).toEqual(ivyEnabled ? ['Declare'] : ['Insert']);
log.length = 0;
// Expect transplanted LView to be CD because the declaration is CD.
expect(trim(fixture.nativeElement.textContent))
.toEqual('DeclareComp(Angular) InsertComp(Hello) Hello Angular!');
insertComp.greeting = 'Hi';
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Declare']);
fixture.detectChanges(false);
expect(log).toEqual(ivyEnabled ? ['Declare'] : ['Insert']);
log.length = 0;
// expect no change because it is on push.
expect(trim(fixture.nativeElement.textContent))
.toEqual('DeclareComp(Angular) InsertComp(Hello) Hello Angular!');
insertComp.changeDetectorRef.markForCheck();
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Declare', 'Insert']);
fixture.detectChanges(false);
expect(log).toEqual(ivyEnabled ? ['Declare', 'Insert'] : ['Insert']);
log.length = 0;
expect(trim(fixture.nativeElement.textContent))
.toEqual('DeclareComp(Angular) InsertComp(Hi) Hi Angular!');
@ -1211,15 +1225,15 @@ describe('change detection', () => {
// Destroy insertion should also destroy declaration
appComp.showInsert = false;
insertComp.changeDetectorRef.markForCheck();
fixture.detectChanges();
ivyEnabled && expect(log).toEqual([]); // No update in declaration
fixture.detectChanges(false);
expect(log).toEqual([]);
log.length = 0;
expect(trim(fixture.nativeElement.textContent)).toEqual('DeclareComp(Angular)');
// Restore both
appComp.showInsert = true;
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Insert']);
fixture.detectChanges(false);
expect(log).toEqual(['Insert']);
log.length = 0;
expect(trim(fixture.nativeElement.textContent))
.toEqual('DeclareComp(Angular) InsertComp(Hello) Hello Angular!');
@ -1228,8 +1242,8 @@ describe('change detection', () => {
appComp.showDeclare = false;
insertComp.greeting = 'Hello';
insertComp.changeDetectorRef.markForCheck();
fixture.detectChanges();
ivyEnabled && expect(log).toEqual(['Insert']);
fixture.detectChanges(false);
expect(log).toEqual(['Insert']);
log.length = 0;
expect(trim(fixture.nativeElement.textContent)).toEqual('InsertComp(Hello) Hello Angular!');
});

View File

@ -23,6 +23,9 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "DECLARATION_COMPONENT_VIEW"
},
{
"name": "DECLARATION_VIEW"
},
@ -557,6 +560,9 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "refreshTransplantedViews"
},
{
"name": "refreshView"
},

View File

@ -23,6 +23,9 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "DECLARATION_COMPONENT_VIEW"
},
{
"name": "DECLARATION_VIEW"
},
@ -413,6 +416,9 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "refreshTransplantedViews"
},
{
"name": "refreshView"
},

View File

@ -29,6 +29,9 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "DECLARATION_COMPONENT_VIEW"
},
{
"name": "DECLARATION_LCONTAINER"
},
@ -1097,6 +1100,9 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "refreshTransplantedViews"
},
{
"name": "refreshView"
},
@ -1244,6 +1250,9 @@
{
"name": "setIsNotParent"
},
{
"name": "setLContainerActiveIndex"
},
{
"name": "setMapAsDirty"
},