20 May 2024
9 min

Change Detection Big Picture – Operations

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:

  1. executing a template function in update mode for the current view
    1. checks and updates input properties on a child component/directive instance
    2. execute the hooks on a child component ngOnInit, ngDoCheck and ngOnChanges if bindings changed
    3. updates DOM interpolations for the current view if properties on current view component instance changed
  2. executeCheckHooks if they have not been run in the previous step
    1. calls OnChanges lifecycle hook on a child component if bindings changed
    2. calls ngDoCheck on a child component (OnInit is called only during first check)
  3. markTransplantedViewsForRefresh
    1. find transplanted views that need to be refreshed down the LView chain and mark them as dirty
  4. refreshEmbeddedViews
    1. runs change detection for views created through ViewContainerRef APIs (mostly repeats the steps in this list)
  5. refreshContentQueries
    1. updates ContentChildren query list on a child view component instance
  6. execute Content CheckHooks (AfterContentInit, AfterContentChecked)
    1. calls AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  7. processHostBindingOpCodes
    1. checks and updates DOM properties on a host DOM element added through @HostBinding() syntax inside the component class
  8. refreshChildComponents
    1. runs change detection for child components referenced in the current component’s template. OnPush components are skipped if they are not dirty
  9. executeViewQueryFn
    1. updates ViewChildren query list on the current view component instance
  10. execute View CheckHooks (AfterViewInit, AfterViewChecked)
    1. calls AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)

Here’s the chart that illustrates the operations listed above:

Image alt

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:

Image alt

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.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.