Operations
When Angular runs change detection for a particular component (view) it performs a number of operations.
Those operations are sometimes referred to as side effects, as in a side effect of the computation logic:
In computer science, an operation, function or expression is said to have a side effect if it modifies
some state variable value(s) outside its local environment, which is to say if it has any observable effect other
than its primary effect of returning a value to the invoker of the operation.
In Angular, the primary side effect of change detection is rendering application state to the target platform.
Most often the target platform is a browser, application state has the form of a component properties
and rendering involves updating the DOM.
When checking a component Angular runs a few other operations.
We can identify them by exploring the refreshView function.
A bit simplified function body with my explanatory comments looks like this:
function refreshView(tView, lView, templateFn, context) {
enterView(lView);
try {
if (templateFn !== null) {
// update input bindings on child components
// execute ngOnInit, ngOnChanges and ngDoCheck hooks
// update DOM on the current component
executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);
}
// execute ngOnInit, ngOnChanges and ngDoCheck hooks
// if they haven't been executed from the template function
const preOrderCheckHooks = tView.preOrderCheckHooks;
if (preOrderCheckHooks !== null) {
executeCheckHooks(lView, preOrderCheckHooks, null);
}
// First mark transplanted views that are declared in this lView as needing a refresh at their
// insertion points. This is needed to avoid the situation where the template is defined in this
// `LView` but its declaration appears after the insertion component.
markTransplantedViewsForRefresh(lView);
// Refresh views added through ViewContainerRef.createEmbeddedView()
refreshEmbeddedViews(lView);
// Content query results must be refreshed before content hooks are called.
if (tView.contentQueries !== null) {
refreshContentQueries(tView, lView);
}
// execute content hooks (AfterContentInit, AfterContentChecked)
const contentCheckHooks = tView.contentCheckHooks;
if (contentCheckHooks !== null) {
executeCheckHooks(lView, contentCheckHooks);
}
// execute logic added through @HostBinding()
processHostBindingOpCodes(tView, lView);
// Refresh child component views.
const components = tView.components;
if (components !== null) {
refreshChildComponents(lView, components);
}
// View queries must execute after refreshing child components because a template in this view
// could be inserted in a child component. If the view query executes before child component
// refresh, the template might not yet be inserted.
const viewQuery = tView.viewQuery;
if (viewQuery !== null) {
executeViewQueryFn<T>(RenderFlags.Update, viewQuery, context);
}
// execute view hooks (AfterViewInit, AfterViewChecked)
const viewCheckHooks = tView.viewCheckHooks;
if (viewCheckHooks !== null) {
executeCheckHooks(lView, viewCheckHooks);
}
// reset the dirty state after the component is checked
if (!isInCheckNoChangesPass) {
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
// this one is tricky :) requires its own section, we'll explore it later
if (lView[FLAGS] & LViewFlags.RefreshTransplantedView) {
lView[FLAGS] &= ~LViewFlags.RefreshTransplantedView;
updateTransplantedViewCount(lView[PARENT] as LContainer, -1);
}
} finally {
leaveView();
}
}
We’ll take a detailed look at all those operations in the “Inside Rendering Engine” section.
For now, let’s go over the core operations executed during change detection inferred from the function I showed above.
Here’s the list of such operations in the order specified:
- executing a template function in update mode for the current view
- checks and updates input properties on a child component/directive instance
- execute the hooks on a child component
ngOnInit
,ngDoCheck
andngOnChanges
if bindings changed - updates DOM interpolations for the current view if properties on current view component instance changed
- executeCheckHooks if they have not been run in the previous step
- markTransplantedViewsForRefresh
- find transplanted views that need to be refreshed down the LView chain and mark them as dirty
- refreshEmbeddedViews
- runs change detection for views created through
ViewContainerRef
APIs (mostly repeats the steps in this list)
- runs change detection for views created through
- refreshContentQueries
- updates
ContentChildren
query list on a child view component instance
- updates
- execute Content CheckHooks (
AfterContentInit
,AfterContentChecked
)- calls
AfterContentChecked
lifecycle hooks on child component instance (AfterContentInit
is called only during first check)
- calls
- processHostBindingOpCodes
- checks and updates DOM properties on a host DOM element added through
@HostBinding()
syntax inside the component class
- checks and updates DOM properties on a host DOM element added through
- refreshChildComponents
- runs change detection for child components referenced in the current component’s template.
OnPush
components are skipped if they are not dirty
- runs change detection for child components referenced in the current component’s template.
- executeViewQueryFn
- updates
ViewChildren
query list on the current view component instance
- updates
- execute View CheckHooks (
AfterViewInit
,AfterViewChecked
)- calls
AfterViewChecked
lifecycle hooks on child component instance (AfterViewInit
is called only during first check)
- calls
Here’s the chart that illustrates the operations listed above:
Observations
There are few things to highlight based on the operations listed above. The first and most interesting one is that
change detection for the current view is responsible for starting change detection for child views.
Check what is ngTemplateOutlet!
This follows from the refreshChildComponents
operation (#8 in the list above).
For each child component Angular executes the
refreshComponent
function:
function refreshComponent(hostLView, componentHostIdx) {
const componentView = getComponentLViewByIndex(componentHostIdx, hostLView);
// Only attached components that are CheckAlways
// or OnPush and dirty should be refreshed
if (viewAttachedToChangeDetector(componentView)) {
const tView = componentView[TVIEW];
if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
refreshView(tView, componentView, tView.template, componentView[CONTEXT]);
} else if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
// Only attached components that are CheckAlways
// or OnPush and dirty should be refreshed
refreshContainsDirtyView(componentView);
}
}
}
There’s a condition that defines if a component will be checked:
if (viewAttachedToChangeDetector(componentView)) { ... }
if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { ... }
The primary condition is that component’s changeDetectorRef
has to be attached to the components tree.
If it’s not attached, neither the component itself, nor its children or containing transplanted views will be checked.
If the primary condition holds, the component will be checked if it’s not OnPush
or if it’s an OnPush
and is dirty.
There’s a logic at the end of the refreshView
function that resets the dirty flag on a <a href="https://staging.angular.love/deep-dive-into-the-onpush-change-detection-strategy-in-angular/">OnPush</a>
component:
// reset the dirty state after the component is checked
if (!isInCheckNoChangesPass) {
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
And lastly, if the component includes transplanted views they will be checked as well:
if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
// Only attached components that are CheckAlways or OnPush and dirty should be refreshed
refreshContainsDirtyView(componentView);
}
Template function
The job of the executeTemplate
function that Angular runs first during change detection
is to execute the template function from a component’s definition.
This template function is generated by the compiler for each component.
For the A
component:
@Component({
selector: 'a-cmp',
template: `<b-cmp [b]="1"></b-cmp> {{ updateTemplate() }}`,
})
export class A {
ngDoCheck() {
console.log('A: ngDoCheck');
}
ngAfterContentChecked() {
console.log('A: ngAfterContentChecked');
}
ngAfterViewChecked() {
console.log('A: ngAfterViewChecked');
}
updateTemplate() {
console.log('A: updateTemplate');
}
}
the definition looks like this:
import {
ɵɵdefineComponent as defineComponent,
ɵɵelement as element,
ɵɵtext as text,
ɵɵproperty as property,
ɵɵadvance as advance,
ɵɵtextInterpolate as textInterpolate
} from '@angular/core';
export class A {}
A.ɵfac = function A_Factory(t) { return new (t || A)(); };
A.ɵcmp = defineComponent({
type: A,
selectors: [["a-cmp"]],
template: function A_Template(rf, ctx) {
if (rf & 1) {
element(0, "b-cmp", 0);
text(1);
}
if (rf & 2) {
property("b", 1);
advance(1);
textInterpolate(" ", ctx.updateTemplate(), "");
}
},
...
}
);
All the functions from the import are exported with the prefix ɵɵ
identifying them as private.
This template can include various instructions. In our case it includes creation instructions element
and text
executed during the initialization phase, and property
,
advance
and textInterpolate
executed during change detection phase:
function A_Template(rf, ctx) {
if (rf & 1) {
element(0, 'b-cmp', 0);
text(1);
}
if (rf & 2) {
property('b', 1);
advance(1);
textInterpolate(' ', ctx.updateTemplate(), '');
}
}
Those are the instructions that will be executed in sequence during change detection.
Lifecyle hook
Angular components can use lifecycle hook methods to tap into key events in the lifecycle of a component or directive.
A lifecycle that starts when Angular creates the component class and renders the component view along with its child views.
The lifecycle continues with change detection, as Angular checks to see when data-bound properties change,
and updates both the view and the component instance as needed.
The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM.
It’s important to understand that lifecycle hook methods are executed during change detection.
Here’s the full list of available lifecycle methods:
- onChanges
- onInit
- doCheck
- afterContentInit
- afterContentChecked
- afterViewInit
- afterViewChecked
- ngOnDestroy
Out of those onInit
, afterContentInit
and afterViewInit
are only executed during the initial change detection run (first run).
The ngOnDestroy
hook is executed only once before the component is destroyed.
The remaining 4 methods are executed during each change detection cycle:
- onChanges
- doCheck
- afterContentChecked
- afterViewChecked
You can also think of a component’s constructor
as a kind of lifecycle event that is executed when an instance of the component is being created.
However, there’s a huge difference between a constructor and a lifecycle method from the perspective of the component initialization phase.
Angular bootstrap process consists of the two major stages:
- constructing components tree
- running change detection
And the constructor of the component is called when Angular constructs a components tree. All lifecycle hooks including ngOnInit
are called as part of the subsequent change detection phase.
It’s also important to understand that most lifecycle hooks are called on the child component while Angular runs change detection for the current component.
The behavior is a bit different only for the ngAfterViewChecked
hook.
We can use this component’s structure to define a hierarchy of 3 components [A -> B -> C]
to log the order of the method calls:
@Component({
selector: 'a-cmp',
template: `<b-cmp [b]="1"></b-cmp> {{ updateTemplate() }}`,
})
export class A {
ngDoCheck() {
console.log('A: ngDoCheck');
}
ngAfterContentChecked() {
console.log('A: ngAfterContentChecked');
}
ngAfterViewChecked() {
console.log('A: ngAfterViewChecked');
}
updateTemplate() {
console.log('A: updateTemplate');
}
}
@Component({
selector: 'b-cmp',
template: `<c-cmp [b]="1"></b-cmp> {{updateTemplate()}}`,
})
export class B {}
@Component({
selector: 'c-cmp',
template: `{{ updateTemplate() }}`,
})
export class C {}
here is the order of hooks calls and bindings updates:
Entering view: A
B: updateBinding
B: ngOnChanges
B: ngDoCheck
A: updateTemplate
B: ngAfterContentChecked
Entering view: B
С: updateBinding
C: ngOnChanges
С: ngDoCheck
B: updateTemplate
С: ngAfterContentChecked
Entering view: C
С: updateTemplate
С: ngAfterViewChecked
B: ngAfterViewChecked
A: ngAfterViewChecked
Check it here.
As you can see, when Angular is running check for the A
component, the lifecycle methods ngOnChanges
and ngDoCheck
are executed on the B
component. That’s a bit unexpected, but perfectly logical.
When Angular runs change detection for A
component, it runs instructions inside its template,
which update bindings on the child B
component.
So once the properties on the B component are updated, it makes sense to notify B
component
about it by running ngOnChanges
on the component.
View and Content Queries
View and Content queries is something we use to get a hold of the elements in a components template in runtime.
Usually the result of the evaluated query is available in the ngAfterViewChecked
or ngAfterContentChecked
lifecycle method.
If we looks at the operations order we can see why:
// Content query results must be refreshed before content hooks are called.
if (tView.contentQueries !== null) {
refreshContentQueries(tView, lView);
}
// execute content hooks (AfterContentInit, AfterContentChecked)
const contentCheckHooks = tView.contentCheckHooks;
if (contentCheckHooks !== null) {
executeCheckHooks(lView, contentCheckHooks);
}
...
// View query results must be refreshed before content hooks are called.
const viewQuery = tView.viewQuery;
if (viewQuery !== null) {
executeViewQueryFn<T>(RenderFlags.Update, viewQuery, context);
}
// execute view hooks (AfterViewInit, AfterViewChecked)
const viewCheckHooks = tView.viewCheckHooks;
if (viewCheckHooks !== null) {
executeCheckHooks(lView, viewCheckHooks);
}
Those refer the operations #5
& #6
for Content Queries, and #9
& #10
for View Queries in the list above.
Angular updates the query results by running the function defined through the component definition.
For the component definition like this
@Component({
selector: 'c-cmp',
template: ``,
})
export class C {
@ViewChild('ctrl') viewChild: any;
@ContentChild('ctrl') contentChild: any;
title = 'c-comp is here';
}
the function to update queries looks like this:
C.ɵcmp = defineComponent({
type: C,
selectors: [["c-cmp"]],
contentQueries: function C_ContentQueries(rf, ctx, dirIndex) {
if (rf & 1) {
contentQuery(dirIndex, _c0, 5);
}
if (rf & 2) {
let _t;
queryRefresh(_t = ["loadQuery"]()) && (ctx.contentChild = _t.first);
}
},
viewQuery: function C_Query(rf, ctx) {
if (rf & 1) {
viewQuery(_c0, 5);
}
if (rf & 2) {
let _t;
queryRefresh(_t = ["loadQuery"]()) && (ctx.viewChild = _t.first);
}
},
template: function C_Template(rf, ctx) {},
...
});
Embedded views
Angular provides a mechanism for implementing dynamic behavior in a component’s template through the use of view containers.
You create a view container using ng-container
tag inside the components template and access it using @ViewChild
query.
These containers provide a way to instantiate a template code, allowing it to be reused and easily modified as needed.
View container provides API to create, manipulate and remove dynamic views.
I call them dynamic views as opposed to static views created by the framework for static components found in templates.
Angular doesn’t use a view container for static views and instead holds a reference to child views inside the node specific to the child component.
We’ll talk more about the differences in view types in the section “Inside rendering engine”.
Embedded views are created from templates by instantiating a TemplateRef
with the viewContainerRef.createEmbeddedView()
method.
View containers can also contain host views which are created by instantiating a component with the createComponent()
method.
A view container instance can contain other view containers, creating a view hierarchy.
All structural directives like ngIf
and ngFor
use a view container to create dynamic views from the directive’s template.
These embedded views are checked during step #4
in the list:
// Refresh views added through ViewContainerRef.createEmbeddedView()
refreshEmbeddedViews(lView);
For the template like this:
<span>My component</span>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{$implicit: greeting}"
>
</ng-container>
<a-comp></a-comp>
<ng-template>
<span>I am an embedded view</span>
<ng-template></ng-template
></ng-template>
The view nodes inside LView
could be represented like this:
There’s a special case of an embedded view called transplanted view.
A transplanted view is an embedded view whose template is declared outside of the template of the component hosting the embedded view.
The component that declares a template with <ng-template>
is not the same component
that uses a view container to insert the embedded view created with this template.
In the following example, a template is declared inside AppComp
, but is rendered inside LibComp
component,
so the embedded view created with this template is considered transplanted:
@Component({
selector: 'lib-comp',
template: `
LibComp: {{ greeting }}!
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: greeting }"
>
</ng-container>
`,
})
class LibComp {
@Input()
template: TemplateRef;
greeting: string = 'Hello';
}
@Component({
template: `
AppComp: {{ name }}!
<ng-template #myTmpl let-greeting> {{ greeting }} {{ name }}! </ng-template>
<lib-comp [template]="myTmpl"></lib-comp>
`,
})
class AppComp {
name: string = 'world';
}
The operation #3 markTransplantedViewsForRefresh
processes updates for such views.