Ivy Research - New Angular Compiler

Original author: Uri Shaked
  • Transfer
I think compilers are very interesting ,” says Uri Sheked, the author of the material, the translation of which we are publishing today. Last year, he wrote an article in which he talked about the reverse engineering of the Angular compiler and about the imitation of certain stages of the compilation process, which helps to understand the features of the internal structure of this mechanism. It should be noted that usually what the author of this material refers to as a “compiler” is called a “rendering engine”.

When Uri heard that a new version of the Angular compiler, called Ivy, was released, he immediately wanted to take a closer look at it and find out what changed in it compared to the old version. Here, just as before, templates and components created by Angular, which are converted into plain HTML and JavaScript code, understandable by Chrome and other browsers, come to the input of the compiler. If we compare the new version of the compiler with the previous one, it turns out that Ivy uses the tree-shaking algorithm. This means that the compiler automatically removes unused code fragments (this also applies to Angular code), reducing the size of project bundles. Another improvement concerns the fact that now each file is compiled independently, which reduces the recompile time. In a nutshell, thanks to the new



to the compiler, we get smaller builds, accelerated recompilation of projects is simpler, ready-made code.

Understanding how the compiler works is interesting in itself (at least, the author of the material hopes for it), but it also helps to better understand the internal mechanisms of Angular. This leads to the improvement of the skills of “Angular-thinking”, which, in turn, makes it possible to more effectively use this framework for web development.

By the way, do you know why the new compiler was called Ivy? The fact is that this word sounds like a combination of the letters "IV", read aloud, which represents the number 4, written in Roman numerals. "4" is the fourth generation of Angular compilers.

Ivy application


Ivy is still in the process of intensive development, you can watch this process here . Although the compiler itself is not yet suitable for use in combat conditions, the RendererV3 abstraction, which it will use, is already quite functional and comes with Angular 6.x.

Although Ivy is not quite ready yet, we can still look at the results of his work. How to do it? Creating a new Angular project:

ng new ivy-internals

After that, you need to enable Ivy, adding the following lines to the file tsconfig.jsonlocated in the new project folder:

"angularCompilerOptions": {
  "enableIvy": true
}

And finally, we run the compiler by executing the command ngcin the newly created project folder:

node_modules/.bin/ngc

That's all. Now you can explore the generated code, located in the folder dist/out-tsc. For example, take a look at the following template fragment AppComponent:

<divstyle="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <imgwidth="300"alt="Angular Logo"src="…"></div>

Here are some links to help you start:


The code generated for this template can be found by looking at the file dist/out-tsc/src/app/app.component.js:

i0.ɵE(0, "div", _c0);
 i0.ɵE(1, "h1");
 i0.ɵT(2);
 i0.ɵe();
 i0.ɵE(3, "img", _c1);
 i0.ɵe();
 i0.ɵe();
 i0.ɵE(4, "h2");
 i0.ɵT(5, "Here are some links to help you start: ");
 i0.ɵe();

It is in this JavaScript code that Ivy transforms the component template. Here's how the same thing was done in the previous version of the compiler:


The code that produces the previous version of the Angular compiler

It seems that the code that generates Ivy is much easier. You can experiment with the component template (it is insrc/app/app.component.html), compile it again and see how the changes made to it affect the generated code.

Parsing the generated code


Let's try to parse the generated code and see what actions it performs. For example, look for an answer to a question about the meaning of calls like i0.ɵEand i0.ɵT.

If you look at the beginning of the generated file, there we will find the following expression:

var i0 = require("@angular/core");

Thus, i0it is just an Angular kernel module, and all of these are functions exported by Angular. The letter is ɵused by the Angular development team to indicate that some methods are intended solely to provide internal framework mechanisms , that is, users should not call them directly, since the API of these methods will not be invariable when new versions of Angular are released (in fact, I’m would say that their API is almost guaranteed to change).

So all these methods are private APIs exported by Angular. It’s easy to understand their functionality by opening a project in VS Code and analyzing the tooltips:


Code Analysis in VS Code

Even though a JavaScript file is analyzed here, VS Code uses type information from TypeScript to identify the call signature and find the documentation for a particular method. If, selecting the method name, use the Ctrl + click combination (Cmd + click on Mac), we will know that the real name of this method iselementStart.

This technique made it possible to find out that the name of the methodɵTistext, the name of the methodɵeiselementEnd. Armed with this knowledge, we can "translate" the generated code, turning it into something that will be easier to read. Here is a small fragment of such a "translation":

var core = require("angular/core");
//...
core.elementStart(0, "div", _c0);
core.elementStart(1, "h1");
core.text(2);
core. ();
core.elementStart(3, "img", _c1);
core.elementEnd();
core.elementEnd();
core.elementStart(4, "h2");
core.text(5, "Here are some links to help you start: ");
core.elementEnd();

And, as already mentioned, this code corresponds to the following text from the HTML template:

<divstyle="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <imgwidth="300"alt="Angular Logo"src="…"></div>

Here are some links to help you start:


After analyzing this all, it is easy to notice the following:

  • Each opening HTML tag corresponds to a call core.elementStart().
  • The closing tags correspond to calls core.elementEnd().
  • Text nodes correspond calls core.text().

The first argument of methods elementStartand texta number, the value of which increases with each call. It is probably an index in some kind of array in which Angular stores references to the created elements.

The elementStartthird argument is also passed to the method . After studying the above materials, we can conclude that the argument is optional and contains a list of attributes for the DOM node. You can check this by looking at the value _c0and finding out that it contains a list of attributes and their values ​​for the element div:

var _c0 = ["style", "text-align:center"];

NgComponentDef note


So far, we have analyzed the part of the generated code that is responsible for rendering the template for the component. This code is actually in a larger code snippet that is assigned AppComponent.ngComponentDef— a static property that contains all the metadata about the component, such as CSS selectors, its change detection strategy (if specified), and the template. If you feel adventurous, you can now figure out how it works, although we'll talk about it below.

Homemade ivy


Now that we, in general terms, understand what the generated code looks like, we can try to create, from scratch, our own component using the same RendererV3 API that Ivy uses.

The code that we are going to create will be similar to the code that the compiler produces, but we will make it so that it is easier to read.

Let's start by writing a simple component, and then manually translate it into code, similar to the one that comes out of the Ivy output:

import { Component } from'@angular/core';
@Component({
  selector: 'manual-component',
  template: '<h2><font color="#3AC1EF">Hello, Component</font></h2>',
})
exportclassManualComponent {
}

The compiler accepts decorator information as input @component, creates instructions, and then draws it all in the form of a static component class property. Therefore, in order to imitate the activity of Ivy, we remove the decorator @componentand replace it with the static property ngComponent:

import * as core from'@angular/core';
exportclassManualComponent{
  static ngComponentDef = core.ɵdefineComponent({
    type: ManualComponent,
    selectors: [['manual-component']],
    factory: () =>new ManualComponent(),
    template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
      // Сюда попадает скомпилированный шаблон
    },
  });
}

We define the metadata for the compiled component by calling ɵdefineComponent. Metadata includes the type of component (used earlier for dependency injection), the CSS selector (or selectors) that will call this component (in our case manual-component, the name of the component in the HTML template), the factory that returns the new component instance, and then the function that defines the pattern for the component. This template displays a visual representation of the component and updates it when the properties of the component change. To create this template, we will use the methods that we found above: ɵE, ɵeand ɵT.

    template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
      core.ɵE(0, 'h2');                 // открываем тег элемента h2
      core.ɵT(1, 'Hello, Component');   // Добавляем текст
      core.ɵe();                        // Закрываем тег элемента h2
    },

At this stage, we do not use the parameters rfor ctfprovided by our template function. We will come back to them. But first, let's look at how to bring our first homemade component to the screen.

First application


In order to display components on the screen, Angular exports a method called ɵrenderComponent. All that needs to be done is to check that the file index.htmlhas an HTML tag corresponding to the element selector <manual-component>, and then add the following to the end of the file:

core.ɵrenderComponent(ManualComponent);

That's all. Now we have a minimal self-made Angular application consisting of only 16 lines of code. You can experiment with the finished application on StackBlitz .

Change detection mechanism


So, we have a working example. Can you add interactivity to it? Say, how about something interesting, like using the Angular change detection system here?

Modify the component so that the user can customize the greeting text. That is, instead of the component always displaying the text Hello, Component, we are going to allow the user to change the part of the text that comes after Hello.

We start by adding a property nameand method to update the value of this property to the component class:

export classManualComponent {
  name = 'Component';
  updateName(newName: string) {
    this.name = newName;
  }
  
  // ...
}

So far all this does not look particularly impressive, but the most interesting is ahead.

Next, we edit the template function so that, instead of the unchanged text, it displays the contents of the property name:

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {   // Создание: вызывается только при первом выводе
    core.ɵE(0, 'h2'); 
    core.ɵT(1, 'Hello, ');
    core.ɵT(2);   // <-- Местозаполнитель для name
    core.ɵe();
  }
  if (rf & 2) {   // Обновление: выполняется при каждом обнаружении изменения
   core.ɵt(2, ctx.name);  // ctx - это экземпляр нашего компонента
  }
},

You may have noticed that we wrapped the template instructions into expressions ifthat validate the values rf. This parameter is used by Angular to indicate whether the component is being created for the first time (the least significant bit will be set ), or we just need to update the dynamic content during the change detection process (this is the second expression that is directed if).

So, when the component is displayed for the first time, we create all the elements, and then, when changes are detected, we only update what could have changed. The internal method is responsible for this ɵt(note the lowercase letter t), which corresponds to the function textBindingexported by Angular:


The textBinding function

So, the first parameter is the index of the element that needs to be updated, the second is the value. In this case, we create an empty text element with the index 2 commandcore.ɵT(2);. It plays the role of a placeholder forname. We update it with the commandcore.ɵt(2, ctx.name);  when a change in the corresponding variable is detected.

At the moment, when this component isdisplayed, the text will still appearHello, Component, although we can change the value of the propertyname, which will change the text on the screen.

To make the application truly interactive, we will add a data entry field with an event listener that calls the component methodupdateName():

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {
    core.ɵE(0, 'h2'); 
    core.ɵT(1, 'Hello, ');
    core.ɵT(2);
    core.ɵe();
    core.ɵT(3, 'Your name: ');
    core.ɵE(4, 'input'); 
    core.ɵL('input', $event => ctx.updateName($event.target.value));
    core.ɵe();
  }
  // ...
},

Event binding is performed in a string core.ɵL('input', $event => ctx.updateName($event.target.value));. Namely, the method ɵLis responsible for setting the event listener for the most recent of the declared elements. The first argument is the name of the event (in this case input, the event that is triggered when the content of the element changes <input>), the second argument is the callback. This callback accepts event data as an argument. Then we retrieve the current value from the target element of the event, that is, from the element <input>, and pass it to the function in the component.

The above code is equivalent to writing the following HTML code in a template:

Your name: <input (input)="updateName($event.target.value)" />

Now you can edit the contents of the element <input>and observe how the text in the component changes. However, the input field is not filled when the component is loaded. In order for everything to work that way, you need to add another instruction to the template function code that is executed when a change is detected:

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) { ... }
  if (rf & 2) {
    core.ɵt(2, ctx.name);
    core.ɵp(4, 'value', ctx.name);
  }
}

Here we use another built-in method of the rendering system ɵp, which updates the property of the element with the specified index. In this case, the method is passed an index of 4, which is the index that is assigned to the element input, and instructs the method that it should place the value ctx.namein the property of valuethis element.

Now our example is finally ready. We implemented, from scratch, two-way data binding using the Ivy rendering system API. This is just great.
Here you can experiment with ready-made code.

Now we are familiar with most of the basic building blocks of the new Ivy compiler. We know how to create elements and text nodes, how to bind properties and configure event listeners, how to use the change detection system.

About * ngIf and * ngFor blocks


Before we finish the Ivy study, consider another interesting topic. Namely, let's talk about how the compiler works with subpatterns. These are the patterns that are used for blocks *ngIfor *ngFor. They are processed in a special way. Let's look at how to use *ngIfin the code of our self-made template.

First you need to install the npm-package @angular/common- it is here that is announced *ngIf. Next, you need to import the directive from this package:

import { NgIf } from '@angular/common';

Now, in order to be able to use NgIfin the template, you need to provide it with some metadata, since the module was @angular/commonnot compiled using Ivy (at least during the writing of the material, and in the future this will probably change with the introduction of ngcc ).

We are going to use a method ɵdefineDirectivethat is related to the method we already know ɵdefineComponent. It defines metadata for directives:

(NgIf as any).ngDirectiveDef = core.ɵdefineDirective({
  type: NgIf,
  selectors: [['', 'ngIf', '']],
  factory: () =>new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()),
  inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'}
});

I found this definition in the Angular source code , along with the declaration ngFor. Now that we have been prepared NgIffor use in Ivy, we can add the following to the list of directives for the component:

static ngComponentDef = core.ɵdefineComponent({
  directives: [NgIf],
  // ...
});

Next, we define a subpattern only for the limited section *ngIf.

Suppose you need to display an image here. Let us define a new function for this template inside the template function:

functionifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {
  if (rf & 1) {
    core.ɵE(0, 'div');
    core.ɵE(1, 'img', ['src', 'https://pbs.twimg.com/tweet_video_thumb/C80o289UQAAKIqp.jpg']);
    core.ɵe();
  }
}

This feature of the template is no different from the one we have already written. Here the same constructions are used to create an element imginside the element div.

And finally, we can put it all together by adding a directive ngIfto the component template:

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {
    // ...
    core.ɵC(5, ifTemplate, null, ['ngIf']);
  }
  if (rf & 2) {
    // ...
    core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));
  }
  functionifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {
    // ...
  }
},

Note the call to the new method at the beginning of the code ( core.ɵC(5, ifTemplate, null, ['ngIf']);). It declares a new container element, that is, an element that has a template. The first argument is the element index; we have already seen such indexes. The second argument is the sub-template function that we have just defined. It will be used as a template for the container element. The third parameter is the tag name for the element, which does not make sense here, and, finally, there is a list of directives and attributes associated with this element. It is here that comes into action ngIf.

The line core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));updates the state of the element by binding the attribute ngIfto the value of the logical expression ctx.name === 'Igor'. It checks whether the namecomponent property is equal to a string Igor.

The above code is equivalent to the following HTML code:

<div *ngIf="name === 'Igor'">
  <imgalign="center"src="..."></div>

Here it can be noted that the new compiler produces not the most compact code, but it is not so bad in comparison with what is now.

You can experiment with a new example here . To see the section NgIfin action, enter the name Igorin the field Your name.

Results


We pretty much traveled on the features of the Ivy compiler. Hope this trip has sparked your interest in further Angular research. If this is so, then now you have everything you need to experiment with Ivy. Now you know how to "translate" templates in JavaScript, how to access the same Angular mechanisms that Ivy uses, without using this compiler itself. I think all this will give you the opportunity to explore the new mechanisms of Angular as deeply as you like.

Here , here and here - three materials in which you can find useful information about the Ivy. And here is the source code for Render3.

Dear readers! How do you feel about the new features Ivy?


Also popular now: