Update:

Give Ivy jit mode a shot

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


Time to peek at what Angular has in store for us

Angular Ivy change detection execution: are you prepared? — figure 1

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

Angular Ivy change detection execution: are you prepared? — figure 2

**Disclaimer:**** this is just my personal exploration of the new Angular renderer**

Angular Ivy change detection execution: are you prepared? — figure 3

The Path of the Angular View Engine

Given that the fledgling Ivy renderer isn't fully fleshed out yet, there's plenty of curiosity about its inner workings and the shifts it will bring.

My aim here is to map out the Ivy change detection flow, highlight features that genuinely excite me, and craft a minimal application built on instruction-like primitives, mirroring Angular Ivy's approach, starting from zero.


First off, let's set up the application under examination:

Angular Ivy change detection execution: are you prepared? — figure 4

@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've put together a live demo to help grasp the mechanics beneath the surface:

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

Angular Ivy change detection execution: are you prepared? — figure 5

The demo makes use of the angular 6.0.1 aot compiler. Feel free to click any lifecycle block to jump to its definition.

To kick off change detection, simply enter text into one of the inputs situated below Sub-Child.

View

Naturally, the view stands as the core low-level abstraction in Angular.

For our scenario, the outcome resembles:

Root view
   |
   |___ AppComponent view
          |
          |__ ChildComponent view
                 |
                 |_ Embedded view
                 |       |
                 |       |_ SubChildComponent view
                 |
                 |_ Embedded view
                 |       |
                 |       |_ SubChildComponent view
                 |
                 |_ SubChildComponent view

A view is meant to depict a template, thus it carries data that mirrors the template’s structure.

Let’s examine the ChildComponent view. Its template is:

<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>

Angular Ivy change detection execution: are you prepared? — figure 6

**Ivy generates LNodes** from instructions embedded in the ngComponentDef.template function, and **saves** them into the **data** array:

Angular Ivy change detection execution: are you prepared? — figure 7

Alongside nodes, the fresh view packs bindings into the **data** array (check data[4], data[5], data[6] in the above figure). Every binding tied to a view is stored sequentially based on its template order, starting from bindingStartIndex.

Notice how the view instance is obtained from ChildComponent. **ComponentInstance.__ngHostLNode__**** holds a reference to the component's host node. (Injecting ChangeDetectorRef is another route)**

Thus, Angular establishes the root view first and drops the host element at index 0 in the data array

RootView
   data: [LNode]
             native: root component selector

then iterates over all components, populating the **data** array for each individual view.

Change detection

The familiar ChangeDetectorRef is essentially an abstract class featuring abstract methods such as detectChanges, markForCheck, and others.

Angular Ivy change detection execution: are you prepared? — figure 8

When this dependency is requested in a component constructor, what actually gets injected is a **ViewRef** instance, which extends the ChangeDetectorRef class.

Now, let's dive into the internal routines responsible for executing change detection in Ivy. A few are exposed via the public API (markViewDirty and detectChanges), but the rest remain uncertain to me.

Angular Ivy change detection execution: are you prepared? — figure 9

detectChanges

This method performs change detection synchronously for a given component and its child components.

Calling this function runs change detection synchronously on a component. Direct invocation is rarely necessary since the recommended approach is to call **use markDirty**and let the scheduler handle the actual change detection at a later point. This design stems from the fact that a single user interaction can invalidate many components, and running change detection synchronously for each one would be wasteful. A better strategy is to wait until all components are marked as dirty and then execute one unified change detection pass across them.

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

This executes change detection across the entire application.

It functions like `detectChanges` but operates on the root component. Furthermore, `tick` runs lifecycle hooks and conditionally evaluates components based on their `ChangeDetectionStrategy` and whether they are dirty.

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

This schedules change detection for the whole application. In contrast to tick, scheduleTick merges multiple invocations into a single change detection cycle. This is typically triggered indirectly when markDirty is called to signal that a view needs re-rendering.

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)

This flags the current view and all its parent views as dirty.

While in Angular 5 this operation simply traversed upward and enabled checks for all parent views, it's crucial to understand that now **markForCheck actively triggers a 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

This flags a component as dirty, indicating it needs change detection.

This action schedules a future change detection run for the component. If the component is already marked as dirty, calling this again has no effect. A single change detection can be scheduled per component tree. Moreover, components bootstrapped separately using `renderComponent` have their own independent schedulers.

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

checkNoChanges

No new developments here:)


While debugging the new change detection mechanism, I discovered that **I had neglected to install zone.js** Surprisingly, everything functioned flawlessly without this dependency and without explicitly calling cdRef.detectChanges or tick. Why is that?

By design, Angular triggers change detection for onPush components only under specific conditions (also discussed in my stackoverflow answer).

These same rules are enforced in Ivy:

In my SubChildComponent, I have an `(input)` output binding. The second rule above causes **markForCheck** to be invoked. Given that this method, as we've established, actually initiates change detection, it becomes clear how this functions without zone.js.

**Is the "Expression has changed after it was checked" error still present?**

Rest assured, it remains:)

Change detection order

Since the announcement of Ivy, the Angular team has diligently worked to ensure the new engine respects the correct order of lifecycle hooks. This means the sequence of operations should remain consistent.

Max NgWizard K astutely observed in his article:

All the well-known operations are present. Yet, the order appears to have been modified. For example, it now seems Angular checks child components before processing embedded views. However, as there's no compiler output yet to test these assumptions, I can't be absolutely certain.

Let's revisit ChildComponent from my simple application.

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

My intention was to place a standard sub-child component before those within an embedded view.

Now, let's observe this in action:

Angular Ivy change detection execution: are you prepared? — figure 10

We can observe that Angular processes the embedded view first, followed by the regular component, which is consistent with the previous engine's behavior.

Interestingly, you can use the optional "run Angular compiler" button in my demo to test various other scenarios.

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

One-time string initialization

Consider a component that receives a color value as a string input. Now, what if we need to pass a constant string that will remain unchanged?

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

This is known as one-time string initialization, and the angular documentation states:

Angular sets it and forgets about it.

To me, this implies Angular won't perform any further checks on this binding. However, in Angular 5, we can see it's checked during every change detection cycle within the updateDirectives call.

function updateDirectives(_ck,_v) {
   var currVal_0 = '#efefef';
  _ck(_v,1,0,currVal_0);

For more details, check out the excellent article “Getting to Know the @Attribute Decorator in Angular” on this topic by Netanel Basal

Now, let's examine how the new engine handles this:

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 depicted, the Angular compiler stores the constant outside the code used for component's creation and update logic, and **only references this value during the creation phase.**

Angular no longer creates text nodes for containers

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

Even if you haven't delved deeply into how Angular's ViewContainer operates internally, you might have noticed the following pattern in your devtools:

Angular Ivy change detection execution: are you prepared? — figure 11

In production mode, we see only <!—-->.

In contrast, here is what Ivy outputs:

Angular Ivy change detection execution: are you prepared? — figure 12

While I can't guarantee it with 100% certainty, it appears Ivy will yield a similar outcome once it's stable.

Consequently, the query in the code snippet below

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

will return null since Angular

should no longer retrieve an ElementRef with a native element pointing to the comment DOM node from containers

Incremental DOM(IDOM) from scratch

Quite a while ago, Google introduced the incremental DOM library.

Its purpose is to construct DOM trees and facilitate dynamic updates. It wasn't meant for direct use, but rather as a compilation target for template engines. It seems that **Ivy shares some core concepts with the incremental DOM library**.

Let's build a simple application from the ground up to illustrate the IDOM rendering process. **Demo**

Our app will feature a counter and also display a user name that we type into an input field.

Angular Ivy change detection execution: are you prepared? — figure 13

Let's assume we already have an <input> and a <button> element on our page:

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

Our task is to render dynamic HTML that looks like this:

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

To accomplish this, we'll write elementOpen, elementClose, and text "instructions" (I use this term because Ivy can be thought of as a special type of virtual CPU).

First, we need some helper functions to traverse the node tree:

// 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;
}

Next, we'll define the instructions themselves:

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;
}

Essentially, these functions walk through the DOM nodes and insert a new node at the current position. The text instruction also sets the data property, allowing the browser to display the text value.

To allow our elements to maintain a state, let's introduce a NodeData structure:

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 modify our renderDOM function so it avoids adding a new element to the DOM if one already exists at the current position:

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;
  }

  ...
}

Take note of my /*, key */ comment. It's beneficial for our elements to have a key for identification. For more, see http://google.github.io/incremental-dom/#demos/using-keys

After that, let's add the logic for updating text nodes:

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;
}

We can apply the same pattern to element nodes.

Let's then create a patch function that accepts a **DOM element**, an update function, and some **data** that the function will consume:

function patch(node, fn, data) {
  currentNode = node;

  enterNode();
  fn(data);
  exitNode();
}

Finally, let's run our instructions:

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 is available here

You can also verify with browser tools that only the text node with the changed content is updated:

Angular Ivy change detection execution: are you prepared? — figure 14

The fundamental principle of IDOM is **to leverage the real DOM itself to diff against new trees.**

That's all for now. Thank you for reading…