Angular Ivy change detection execution: are you prepared?

Post Editor

In this article I am going to visualize Ivy change detection mechanism, show some things I am really excited about and also build simple app based on instructions, similar to angular Ivy instructions, from scratch.

11 min read
post

Angular Ivy change detection execution: are you prepared?

In this article I am going to visualize Ivy change detection mechanism, show some things I am really excited about and also build simple app based on instructions, similar to angular Ivy instructions, from scratch.

post
post
11 min read

Update:
Link to this section

Try Ivy jit mode

https://alexzuza.github.io/ivy-jit-preview/ ?


Let’s see what Angular cooks for us

Content imageContent image

Fan of Angular-In-Depth? Support us on Twitter!

Content imageContent image
Disclaimer: it is just my learning journey to new Angular renderer
Content imageContent image
The Evolution of Angular View Engine

While new Ivy renderer is not feature completely yet, many people wonder how it will work and what changes it prepares for us.

In this article I am going to visualize Ivy change detection mechanism, show some things I am really excited about and also build simple app based on instructions, similar to angular Ivy instructions, from scratch.


First, let’s introduce the app I’m going to investigate here:

Content imageContent image
<>Copy
@Component({ selector: 'my-app', template: ` <h2>Parent</h2> <child [prop1]="x"></child> ` }) export class AppComponent { x = 1; } @Component({ selector: 'child', template: ` <h2>Child {{ prop1 }}</h2> <sub-child [item]="3"></sub-child> <sub-child *ngFor="let item of items" [item]="item"></sub-child> ` }) export class ChildComponent { @Input() prop1: number; items = [1, 2]; } @Component({ selector: 'sub-child', template: ` <h2 (click)="clicked.emit()">Sub-Child {{ item }}</h2> <input (input)="text = $event.target.value"> <p>{{ text }}</p> ` }) export class SubChildComponent { @Input() item: number; @Output() clicked = new EventEmitter(); text: string;

I created online demo that I use to understand how it works under the hood:

https://alexzuza.github.io/ivy-cd/

Content imageContent image

The demo uses angular 6.0.1 aot compiler. You can click on any lifecycle block to go to the definition.

In order to run change detection process just type something in one of those inputs that are below Sub-Child.

View
Link to this section

Of course, the view is the main low-level abstraction in Angular.

For our example we will get something like:

<>Copy
Root view | |___ AppComponent view | |__ ChildComponent view | |_ Embedded view | | | |_ SubChildComponent view | |_ Embedded view | | | |_ SubChildComponent view | |_ SubChildComponent view

View should describe template so it contains some data that will reflect structure of that template.

Let’s look at ChildComponent view. It has the following template:

<>Copy
<h2>Child {{ prop1 }}</h2> <sub-child [item]="3"></sub-child> <sub-child *ngFor="let item of items" [item]="item"></sub-child>


<h2>Child {{ prop1 }}</h2><sub-child [item]="3"></sub-child><sub-child *ngFor="let item of items" [item]="item"></sub-child>

Content imageContent image

Ivy creates LNodes from instructions, that are written in ngComponentDef.template function, and stores them in data array:

Content imageContent image

Besides nodes, new view also contains bindings in data array(see data[4], data[5], data[6] in the picture above). All bindings for a given view are stored in the order in which they appear in the template, starting with bindingStartIndex

Note how I get view instance from the ChildComponent. ComponentInstance.__ngHostLNode__ contains reference to the component host node. (Another way is to inject ChangeDetectorRef)

This way angular first creates root view and locate host element at index 0 in data array

<>Copy
RootView data: [LNode] native: root component selector

and then goes through all components and fills data array for each view.

Change detection
Link to this section

Well known ChangeDetectorRef is simply abstract class with abstract methods like detectChanges, markForCheck, etc.

Content imageContent image

When we ask this dependency in component constructor we actually gets ViewRef instance that extends ChangeDetectorRef class.

Now, let’s examine internal methods that are used to run change detection in Ivy. Some of them are available as public api(markViewDirty and detectChanges) but I am unsure about others.

Content imageContent image

detectChanges
Link to this section

Synchronously performs change detection on a component (and possibly its sub-components).

This function triggers change detection in a synchronous way on a component. There should be very little reason to call this function directly since a preferred way to do change detection is to use markDirty(see below) and wait for the scheduler to call this method at some future point in time. This is because a single user action often results in many components being invalidated and calling change detection on each component synchronously would be inefficient. It is better to wait until all components are marked as dirty and then perform single change detection across all of the components
<>Copy
export function detectChanges<T>(component: T): void { const hostNode = _getComponentHostLElementNode(component); ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); const componentIndex = hostNode.tNode !.flags >> TNodeFlags.DirectiveStartingIndexShift; const def = hostNode.view.tView.directives ![componentIndex] as ComponentDef<T>; detectChangesInternal(hostNode.data as LView, hostNode, def, component); }

tick
Link to this section

Used to perform change detection on the whole application.

This is equivalent to `detectChanges`, but invoked on root component. Additionally, `tick` executes lifecycle hooks and conditionally checks components based on their `ChangeDetectionStrategy` and dirtiness.
<>Copy
export function tick<T>(component: T): void { const rootView = getRootView(component); const rootComponent = (rootView.context as RootContext).component; const hostNode = _getComponentHostLElementNode(rootComponent); ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); renderComponentOrTemplate(hostNode, rootView, rootComponent); }

scheduleTick
Link to this section

Used to schedule change detection on the whole application. Unlike tick, scheduleTick coalesces multiple calls into one change detection run. It is usually called indirectly by calling markDirty when the view needs to be re-rendered.

<>Copy
export function scheduleTick<T>(rootContext: RootContext) { if (rootContext.clean == _CLEAN_PROMISE) { let res: null|((val: null) => void); rootContext.clean = new Promise<null>((r) => res = r); rootContext.scheduler(() => { tick(rootContext.component); res !(null); rootContext.clean = _CLEAN_PROMISE; }); } }

markViewDirty(markForCheck)
Link to this section

Marks current view and all ancestors dirty.

Whereas early in Angular 5 it only iterated upwards and enabled checks for all parent views, now please note that markForCheck does trigger change detection cycle in Ivy!!!

export function markViewDirty(view: LView): void {  let currentView: LView|null = view;  while (currentView.parent != null) {    currentView.flags |= LViewFlags.Dirty;    currentView = currentView.parent;  }  currentView.flags |= LViewFlags.Dirty;  ngDevMode && assertNotNull(currentView !.context, 'rootContext');  scheduleTick(currentView !.context as RootContext);}

markDirty
Link to this section

Mark the component as dirty (needing change detection).

Marking a component dirty will schedule a change detection on this component at some point in the future. Marking an already dirty component as dirty is a noop. Only one outstanding change detection can be scheduled per component tree. (Two components bootstrapped with separate `renderComponent` will have separate schedulers)

<>Copy
export function markDirty<T>(component: T) { ngDevMode && assertNotNull(component, 'component'); const lElementNode = _getComponentHostLElementNode(component); markViewDirty(lElementNode.view); }

checkNoChanges
Link to this section

Nothing new:)


When I was debugging new change detection mechanism I noticed that I forgot to install zone.js. And as you have already guessed it worked perfectly without that dependency and without cdRef.detectChanges or tick . Why?

As you probably know by design Angular triggers change detection for onPush component only if (see my answer on stackoverflow).

These rules are also applied to the Ivy:

I have (input) output binding in SubChildComponent. The second rule will result in calling markForCheck. Since we have already learned that this method actually calls change detection it should be clear now how it works without zonejs.

What about Expression has change after it was checked?

Don’t worry, it is still here:)

Change detection order
Link to this section

Since Ivy was announced Angular team has been doing hard work to ensure that the new engine correctly handles all lifecycle hooks in the correct order. That means that the order of operations should be similar.

Max NgWizard K wrote in his great article:

As you can see, all the familiar operations are still here. But the order of operations appears to have changed. For example, it seems that now Angular first checks the child components and only then the embedded views. Since at the moment there’s no compiler to produce output suitable to test my assumptions, I can’t know for sure.

Let’s come back to ChildComponent in my simple app

<>Copy
<h2>Child {{ prop1 }}</h2> <sub-child [item]="3"></sub-child> <sub-child *ngFor="let item of items" [item]="item"></sub-child>

It was intended from my side to write one sub-child as regular component before others that are inside embedded view.

Now it’s time to see it in action:

Content imageContent image

As we can angular first checks embedded view and then regular component. So there is no changes here from previous engine.

Anyway, there is optional “run Angular compiler” button in my demo and we can test other cases.

https://alexzuza.github.io/ivy-cd/

One-time string initialization
Link to this section

Imagine we wrote component that can receive color as string input value. And now we want to pass that input as constant string that will never be changed:

<>Copy
<comp color="#efefef"></comp>

It’s so called one-time string initialization and angular documentation states:

Angular sets it and forgets about it.

As for me, it means that angular won’t do any additional checks for this binding. But what we actually see in angular5 is that it is checked per every change detection cycle during updateDirectives call.

<>Copy
function updateDirectives(_ck,_v) { var currVal_0 = '#efefef'; _ck(_v,1,0,currVal_0);
See also great article “Getting to Know the @Attribute Decorator in Angular” about this issue by Netanel Basal

Now let’s look at how it is supposed to be in new engine:

<>Copy
var _c0 = ["color", "#efefef"]; AppComponent.ngComponentDef = i0.ɵdefineComponent({ type: AppComponent, selectors: [["my-app"]], ... template: function AppComponent_Template(rf, ctx) { // create mode if (rf & 1) { i0.ɵE(0, "child", _c0); <========== used only in create mode i0.ɵe(); } if (rf & 2) { ... } } })

As we can see angular compiler stores our constant outside of the code that is responsible for creating and updating component and only uses this value in create mode.

Angular no longer creates text nodes for containers
Link to this section

Update: https://github.com/angular/angular/pull/24346

Even if you don’t know how angular ViewContainer works under the hood you may noticed the following picture when opening devtools:

Content imageContent image
In production mode we see only <!—-->.

And here’s the Ivy output:

Content imageContent image

I can’t be sure 100% but seems we will have such result once Ivy gets stable.

As a result the query in the code below

<>Copy
@Component({ ..., template: '<ng-template #foo></ng-template>' }) class SomeComponent { @ViewChild('foo', {read: ElementRef}) query; }

will return null since angular

should no longer read ElementRef with a native element pointing to comment DOM node from containers

Incremental DOM(IDOM) from scratch
Link to this section

A long time ago Google announced so-called incremental DOM library.

The library focuses on building DOM trees and allowing dynamic updates. It wasn’t intended to be used directly but as a compilation target for template engines. And seems the IVy has something in common with incremental DOM library.

Let’s build simple app from scratch that will help us to understand how IDOM render works. Demo

Our app will have counter and also print user name that we will by typing in input element.

Content imageContent image

Assume we already have <input> and <button> element on the page:

<>Copy
<input type="text" value="Alexey"> <button>Increment</button>


And all we need to do is to render dynamic html that will look like:

<>Copy
<h1>Hello, Alexey</h1> <ul> <li> Counter: <span>1</span> </li> </ul>

In order to render this let’s write elementOpen, elementClose and text “instructions” (I call it this way because Angular uses such names as IVy can be considered as special kind of virtual CPU).

First we need to write special helpers to traverse nodes tree:

<>Copy
// The current nodes being processed let currentNode = null; let currentParent = null; function enterNode() { currentParent = currentNode; currentNode = null; } function nextNode() { currentNode = currentNode ? currentNode.nextSibling : currentParent.firstChild; } function exitNode() { currentNode = currentParent; currentParent = currentParent.parentNode; }

Now, let’s write instructions:

<>Copy
function renderDOM(name) { const node = name === '#text' ? document.createTextNode('') : document.createElement(name); currentParent.insertBefore(node, currentNode); currentNode = node; return node; } function elementOpen(name) { nextNode(); const node = renderDOM(name); enterNode(); return currentParent; } function elementClose(node) { exitNode(); return currentNode; } function text(value) { nextNode(); const node = renderDOM('#text'); node.data = value; return currentNode; }

Put differently, these functions just walk through DOM nodes and insert node at current position. Also text instruction sets data property so that we can see text value the browser.

We want our elements to be capable of keeping some state, so let’s introduce NodeData:

<>Copy
const NODE_DATA_KEY = '__ID_Data__'; class NodeData { // key // attrs constructor(name) { this.name = name; this.text = null; } } function getData(node) { if (!node[NODE_DATA_KEY]) { node[NODE_DATA_KEY] = new NodeData(node.nodeName.toLowerCase()); } return node[NODE_DATA_KEY]; }

Now, let’s change our renderDOM function so that we won’t add new element to the DOM if there is already the same at current position:

<>Copy
const matches = function(matchNode, name/*, key */) { const data = getData(matchNode); return name === data.name // && key === data.key; }; function renderDOM(name) { if (currentNode && matches(currentNode, name/*, key */)) { return currentNode; } ... }

Note my comment /*, key */ . It would be better if our elements have some key to distinguish elements. See also http://google.github.io/incremental-dom/#demos/using-keys

After that let’s add logic that will be responsible for text node updates

<>Copy
function text(value) { nextNode(); const node = renderDOM('#text'); // update // checks for text updates const data = getData(node); if (data.text !== value) { data.text = (value); node.data = value; } // end update return currentNode; }

The same we can do for element nodes.

Then let’s write patch function that will take DOM element, update function and some data that will be consumed by update function:

<>Copy
function patch(node, fn, data) { currentNode = node; enterNode(); fn(data); exitNode(); };

Finally, let’s test our instructions:

<>Copy
function render(data) { elementOpen('h1'); { text('Hello, ' + data.user) } elementClose('h1'); elementOpen('ul') { elementOpen('li'); { text('Counter: ') elementOpen('span'); { text(data.counter); } elementClose('span'); } elementClose('li'); } elementClose('ul'); } document.querySelector('button').addEventListener('click', () => { data.counter ++; patch(document.body, render, data); }); document.querySelector('input').addEventListener('input', (e) => { data.user = e.target.value; patch(document.body, render, data); }); const data = { user: 'Alexey', counter: 1 }; patch(document.body, render, data);

The result can be found here

You can also verify that the code will only update the text node whose contents has changed by inspecting the with the browser tools:

Content imageContent image

So the main concept of IDOM is to just use the real DOM to diff against new trees.

That’s all. Thanks for reading…

Share

About the author

author_image

Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.

author_image

About the author

Alexey Zuev

Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.

About the author

author_image

Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.

Looking for a JS job?
Job logo
JavaScript Developer (NMMES)

Imagine One Technology & Management, Ltd

United States, Norfolk

$71k - $130k
Job logo
Remote JavaScript Web Developer

MonetizeMore

Worldwide
Remote
$71k - $90k
Job logo
JavaScript Front-End Developer

Oxley Enterprises

United States
Remote
$81k - $103k
Job logo
Senior Javascript Engineer

VersaPay

Worldwide
Remote
$117k - $148k
More jobs

Featured articles

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more
blockchainpostAn Introduction to Blockchain

19 July 2022

12 min read

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more