Core Operations During Change Detection
Earlier, I published a deep dive into the numerous steps Angular's change detection performed. That material became outdated once Angular adopted the Ivy rendering engine in v12. Now I want to walk through the complete set of operations that the new engine executes.
This article is an excerpt from my Angular Deep Dive course series
Every time Angular checks a component view, it executes a sequence of operations. In computer science terms, these are often described as side effects — actions that produce observable changes beyond merely returning a value:
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.
Within Angular, the principal side effect of change detection is projecting application state onto the target platform. In typical browser scenarios, that state consists of component properties and the projection involves modifying the DOM.
Several additional operations happen during component checks. These can be identified through the [refreshView](https://github.com/angular/angular/blob/02f3d12a0dc2c1b6f5ae06fff019058036fa5edc/packages/core/src/render3/instructions/shared.ts#L357%3E) function. Here's an abbreviated version of its body with my annotations:
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();
}
}
Each operation will be examined in detail in the "Inside Rendering Engine" section.
For now, let's survey the core change detection operations derived from the function above, listed in their execution order:
- executing a template function in update mode for the current view
– validates and updates bindings on child components/directives
– triggersngOnInit,ngDoCheck, andngOnChangeson the child when bindings have changed
– refreshes DOM interpolations for the current view when its component properties have changed - executeCheckHooks if they weren't run in the prior step
– invokes theOnChangeslifecycle hook on the child component upon binding changes
– invokesngDoCheckon the child component (OnInitis limited to the first check) - markTransplantedViewsForRefresh
– locates transplanted views further down theLviewchain that need refreshing - refreshEmbeddedViews
– executes change detection for views established through ViewContainerRef APIs (largely repeating the steps in this list) - refreshContentQueries
– refreshes theContentChildrenquery list on the child view component - execute Content CheckHooks
– triggersAfterContentCheckedlifecycle hooks on the child component (AfterContentInitis called only during the first check) - processHostBindingOpCodes
– validates and modifies host DOM properties linked through@HostBinding()declarations within the component class - refreshChildComponents
– runs change detection for child components referenced in the current template. OnPush components are bypassed unless marked dirty - executeViewQueryFn
– refreshes theViewChildrenquery list on the current view's component - execute View CheckHooks (AfterViewInit, AfterViewChecked)
– triggersAfterViewCheckedlifecycle hooks on the child component (AfterViewInitis called only during the first check)
Key Observations
Several insights emerge from these operations.
Change detection for the current view serves as the trigger for child view change detection. This is evident from the refreshChildComponents operation (#8). Angular invokes [refreshComponent](https://github.com/angular/angular/blob/02f3d12a0dc2c1b6f5ae06fff019058036fa5edc/packages/core/src/render3/instructions/shared.ts#L1687) for every child component:
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);
}
}
}
A condition dictates whether a component gets checked:
if (viewAttachedToChangeDetector(componentView)) { ... }
if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {...}
The key requirement is that the component's [changeDetectorRef](https://github.com/angular/angular/blob/c14c701775c900ce9ac8781c08fc76da067910c5/packages/core/src/change_detection/change_detector_ref.ts#L63) must be connected to the component tree. Without that connection, the component itself, its children, and any transplanted views it contains are all skipped.
Provided the primary requirement holds, a component is considered for checking when it's not using OnPush, or when it is an OnPush component marked as dirty. The refreshView function concludes by resetting the dirty flag on an OnPush component:
// reset the dirty state after the component is checked
if (!isInCheckNoChangesPass) {
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
Finally, any transplanted views associated with the component are also subject to checking:
if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
// Only attached components that are CheckAlways or OnPush and dirty should be refreshed
refreshContainsDirtyView(componentView);
}
Template execution
The executeTemplate function, which Angular runs at the start of change detection, is responsible for invoking the template function from the component's definition. The compiler generates this template function for each component individually. Taking component A as an example:
@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');
}
}
its definition appears as:
import {
ɵɵdefineComponent as defineComponent,
ɵɵelement as element,
ɵɵtext as text,
ɵɵproperty as property,
ɵɵadvance as advance,
ɵɵtextInterpolate1 as textInterpolate1
} from '@angular/core';
export class A {}
export class B {}
A.ɵfac = function A_Factory(t) { return new (t || A)(); };
A.ɵcmp = defineComponent({
type: A,
selectors: [["a-cmp"]],
decls: 2,
vars: 2,
consts: [[3, "b"]],
template: function A_Template(rf, ctx) {
if (rf & 1) {
element(0, "b-cmp", 0);
text(1);
}
if (rf & 2) {
property("b", 1);
advance(1);
textInterpolate1(" ", ctx.updateTemplate(), "");
}
},
dependencies: function() { return [B]; },
encapsulation: 2
}
);
Every function imported from the module carries the
__ɵɵ__prefix, marking it as private.
The template may contain various instructions. In this case, we see the creational instructions element and text during initialization, along with property, advance, and textInterpolate1 during the change detection phase:
template: function A_Template(rf, ctx) {
if (rf & 1) {
element(0, "b-cmp", 0);
text(1);
}
if (rf & 2) {
property("b", 1);
advance(1);
textInterpolate1(" ", ctx.updateTemplate(), "");
}
}
Lifecycle hooks
A crucial detail is that the majority of lifecycle hooks are invoked on the child component during the current component's change detection pass. The ngAfterViewChecked hook stands out as the exception.
With a component hierarchy like A -> B -> C, the sequence of hook calls and binding updates is:
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
That concludes this overview. I'm regularly adding new material to the course, including free content such as what I've presented here. Check out the course by clicking here, or read "Early bird option for the most in-depth Angular course" for more details about the content and who it's designed for.
