Eine Studie von Ivy, dem neuen Angular-Compiler

" Ich denke, Compiler sind sehr interessant ", sagt Uri Shaked, der Autor des Materials, das wir heute veröffentlichen. Letztes Jahr schrieb er einen Artikel über das Reverse Engineering des Angular-Compilers und die Simulation einiger Phasen des Kompilierungsprozesses, um die Merkmale der internen Struktur dieses Mechanismus besser zu verstehen. Es sollte beachtet werden, dass das, worüber der Autor dieses Materials als „Compiler“ spricht, normalerweise als „Rendering-Engine“ bezeichnet wird.

Als Uri hörte, dass eine neue Version des Angular-Compilers namens Ivy veröffentlicht wurde, wollte er sofort genauer hinschauen und herausfinden, was sich daran gegenüber der alten Version geändert hatte. Hier erhält der Compiler nach wie vor die von Angular erstellten Vorlagen und Komponenten, die in regulären HTML- und JavaScript-Code konvertiert werden, der für Chrome und andere Browser verständlich ist.



Wenn Sie die neue Version des Compilers mit der vorherigen vergleichen, stellt sich heraus, dass Ivy den Tree-Shaking-Algorithmus verwendet. Dies bedeutet, dass der Compiler nicht verwendete Codefragmente automatisch löscht (dies gilt auch für Angular-Code), wodurch die Größe der Projektpakete verringert wird. Eine weitere Verbesserung betrifft die Tatsache, dass jetzt jede Datei unabhängig kompiliert wird, was die Zeit für die Neukompilierung verkürzt. Kurz gesagt, dank des neuen Compilers erhalten wir kleinere Assemblys, eine beschleunigte Neukompilierung von Projekten und einfacheren vorgefertigten Code.

Zu verstehen, wie der Compiler funktioniert, ist an und für sich interessant (zumindest der Autor des Materials hofft dies), aber es hilft auch, die internen Mechanismen von Angular besser zu verstehen. Dies führt zu einer Verbesserung der Fähigkeiten des "Angular-Denkens", wodurch Sie dieses Framework effektiver für die Webentwicklung nutzen können.

Wissen Sie übrigens, warum der neue Compiler Ivy heißt? Tatsache ist, dass dieses Wort wie eine Kombination von vorgelesenen Buchstaben „IV“ klingt, die die Zahl 4 darstellt, die in römischen Ziffern geschrieben ist. "4" ist die vierte Generation von Angular-Compilern.

Efeu-Anwendung


Ivy befindet sich noch in einer intensiven Entwicklung, dieser Prozess kann hier beobachtet werden . Obwohl der Compiler selbst noch nicht für den Kampfeinsatz geeignet ist, ist die Abstraktion von RendererV3, die er verwenden wird, bereits recht funktionsfähig und wird mit Angular 6.x geliefert.

Obwohl Ivy noch nicht ganz fertig ist, können wir uns noch die Ergebnisse seiner Arbeit ansehen. Wie kann man das machen? Durch Erstellen eines neuen Angular-Projekts:

ng new ivy-internals 

Danach müssen Sie Ivy aktivieren, indem Sie der Datei tsconfig.json im neuen Projektordner die folgenden Zeilen hinzufügen:

 "angularCompilerOptions": { "enableIvy": true } 

ngc Schluss starten wir den Compiler, indem wir den Befehl ngc im neu erstellten Projektordner ausführen:

 node_modules/.bin/ngc 

Das ist alles. Jetzt können Sie den generierten Code im dist/out-tsc . Sehen Sie sich beispielsweise das folgende Fragment der AppComponent Vorlage an:

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

Hier sind einige Links, die Ihnen den Einstieg erleichtern:


Den für diese Vorlage generierten Code finden Sie in der 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(); 

In dieser Art von JavaScript-Code transformiert Ivy die Komponentenvorlage. So wurde in der vorherigen Version des Compilers dasselbe gemacht:


Code, der von einer früheren Version des Angular-Compilers erstellt wurde

Es besteht das Gefühl, dass der von Ivy generierte Code viel einfacher ist. Sie können mit der Komponentenvorlage experimentieren (sie befindet sich in src/app/app.component.html ), sie erneut kompilieren und sehen, wie sich die daran vorgenommenen Änderungen auf den generierten Code auswirken.

Generierten Code analysieren


Versuchen wir, den generierten Code zu analysieren und genau zu sehen, welche Aktionen er ausführt. i0.ɵE i0.ɵT beispielsweise nach einer Antwort auf eine Frage zur Bedeutung von Anrufen wie i0.ɵE und i0.ɵT

Wenn Sie sich den Anfang der generierten Datei ansehen, finden Sie dort den folgenden Ausdruck:

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

i0 ist also nur das Angular-Kernmodul, und all dies sind die von Angular exportierten Funktionen. Der Buchstabe ɵ vom Angular-Entwicklungsteam verwendet, um anzugeben, dass einige Methoden ausschließlich interne Framework- Mechanismen bereitstellen sollen, dh Benutzer sollten sie nicht direkt aufrufen, da die Invarianz der API dieser Methoden nicht garantiert wird, wenn neue Versionen von Angular veröffentlicht werden (tatsächlich) Ich würde sagen, dass sich ihre APIs fast garantiert ändern werden.

Alle diese Methoden sind also private APIs, die von Angular exportiert werden. Es ist einfach, ihre Funktionalität herauszufinden, indem Sie das Projekt in VS Code öffnen und die Tooltips analysieren:


Code-Analyse in VS-Code

Obwohl hier eine JavaScript-Datei analysiert wird, verwendet VS Code Typinformationen von TypeScript, um die Anrufsignatur zu identifizieren und Dokumentation für eine bestimmte Methode zu finden. Wenn Sie nach Auswahl des Methodennamens die Kombination Strg + Klick (Cmd + Klick auf Mac) verwenden, stellen wir fest, dass der tatsächliche Name dieser Methode elementStart .

Diese Technik ermöglichte es herauszufinden, dass der Methodenname ɵT text , der Methodenname ɵe ist elementEnd . Mit diesem Wissen können wir den generierten Code „übersetzen“ und ihn in etwas verwandeln, das bequemer zu lesen ist. Hier ist ein kleines Fragment einer solchen "Übersetzung":

 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(); 

Und wie bereits erwähnt, entspricht dieser Code dem folgenden Text aus der HTML-Vorlage:

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

Hier sind einige Links, die Ihnen den Einstieg erleichtern:


Nach all dieser Analyse fällt Folgendes leicht auf:

  • Jedes öffnende HTML-Tag hat einen Aufruf von core.elementStart() .
  • Die schließenden Tags entsprechen Aufrufen von core.elementEnd() .
  • core.text() entsprechen Aufrufen von core.text() .

Das erste Argument für die Methoden elementStart und text ist eine Zahl, deren Wert mit jedem Aufruf zunimmt. Es stellt wahrscheinlich einen Index in einem Array dar, in dem Angular Links zu erstellten Elementen speichert.

Das dritte Argument wird ebenfalls an die elementStart Methode übergeben. Nachdem wir die obigen Materialien untersucht haben, können wir schließen, dass das Argument optional ist und eine Liste von Attributen für den DOM-Knoten enthält. Sie können dies überprüfen, indem Sie den Wert von _c0 und feststellen, dass er eine Liste von Attributen und deren Werten für das div Element enthält:

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

NgComponentDef Hinweis


Bisher haben wir den Teil des generierten Codes analysiert, der für das Rendern der Vorlage für die Komponente verantwortlich ist. Dieser Code befindet sich tatsächlich in einem größeren Codeteil, der AppComponent.ngComponentDef zugewiesen AppComponent.ngComponentDef - einer statischen Eigenschaft, die alle Metadaten zu der Komponente enthält, z. B. CSS-Selektoren, ihre Änderungserkennungsstrategie (falls angegeben) und die Vorlage. Wenn Sie Lust auf Abenteuer haben, können Sie jetzt selbstständig herausfinden, wie es funktioniert, obwohl wir weiter unten darauf eingehen werden.

Hausgemachter Efeu


Nachdem wir nun allgemein verstanden haben, wie der generierte Code aussieht, können wir versuchen, unsere eigene Komponente mit derselben RendererV3-API, die Ivy verwendet, von Grund auf neu zu erstellen.

Der Code, den wir erstellen werden, ähnelt dem Code, den der Compiler erstellt, aber wir werden ihn so gestalten, dass er leichter zu lesen ist.

Beginnen wir mit dem Schreiben einer einfachen Komponente und übersetzen sie dann manuell in Code, der dem von Ivy ähnelt:

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

Der Compiler verwendet die Eingabe des @component Dekorators als @component , erstellt Anweisungen und ordnet sie dann als statische Eigenschaft der Komponentenklasse an. Um die Aktivität von Ivy zu simulieren, entfernen wir daher den @component Dekorator und ersetzen ihn durch die statische ngComponent Eigenschaft:

 import * as core from '@angular/core'; export class ManualComponent { static ngComponentDef = core.ɵdefineComponent({   type: ManualComponent,   selectors: [['manual-component']],   factory: () => new ManualComponent(),   template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {     //       }, }); } 

Wir definieren Metadaten für die kompilierte Komponente, indem ɵdefineComponent aufrufen. Zu den Metadaten gehören der Komponententyp (der zuvor zum Implementieren der Abhängigkeit verwendet wurde), der CSS-Selektor (oder die Selektoren), der diese Komponente aufruft (in unserem Fall ist die manual-component der Name der Komponente in der HTML-Vorlage), die Factory, die die neue Instanz zurückgibt Komponente und dann die Funktion, die die Vorlage für die Komponente definiert. Diese Vorlage zeigt eine visuelle Darstellung der Komponente an und aktualisiert sie, wenn sich die Eigenschaften der Komponente ändern. Um diese Vorlage zu erstellen, verwenden wir die oben gefundenen Methoden: ɵE , ɵe und ɵT .

     template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {     core.ɵE(0, 'h2');                 //    h2     core.ɵT(1, 'Hello, Component');   //       core.ɵe();                        //    h2   }, 

Zu diesem Zeitpunkt verwenden wir nicht die von unserer Vorlagenfunktion bereitgestellten ctf oder ctf Parameter. Wir werden auf sie zurückkommen. Schauen wir uns zunächst an, wie unsere erste selbstgemachte Komponente auf dem Bildschirm angezeigt wird.

Erste Bewerbung


Um Komponenten auf dem Bildschirm anzuzeigen, exportiert Angular eine Methode namens ɵrenderComponent . Sie müssen index.html überprüfen, ob die Datei index.html ein HTML-Tag enthält, das der Elementauswahl <manual-component> , und am Ende der Datei Folgendes hinzufügen:

 core.ɵrenderComponent(ManualComponent); 

Das ist alles. Jetzt haben wir eine minimale selbst erstellte Angular-Anwendung, die nur aus 16 Codezeilen besteht. Sie können mit der fertigen Anwendung auf StackBlitz experimentieren .

Änderungserkennungsmechanismus


Wir haben also ein funktionierendes Beispiel. Können Sie dem Interaktivität hinzufügen? Sagen wir, wie wäre es mit etwas Interessantem, wie der Verwendung des Änderungserkennungssystems von Angular hier?

Ändern Sie die Komponente so, dass der Benutzer den Begrüßungstext anpassen kann. Das heißt, anstatt dass die Komponente immer den Text Hello, Component anzeigt, lassen wir den Benutzer den Teil des Textes ändern, der nach Hello .

Wir beginnen mit dem Hinzufügen der Eigenschaft name und einer Methode zum Aktualisieren des Werts dieser Eigenschaft für die Komponentenklasse:

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

Das alles sieht zwar nicht besonders beeindruckend aus, aber das Interessanteste liegt vor uns.

Als Nächstes bearbeiten wir die Vorlagenfunktion so, dass anstelle von unveränderlichem Text der Inhalt der Eigenschaft name angezeigt wird:

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

Möglicherweise haben Sie bemerkt, dass wir die Vorlagenanweisungen in if , die die rf Werte überprüfen. Dieser Parameter wird von Angular verwendet, um anzugeben, ob die Komponente zum ersten Mal erstellt wird (das niedrigstwertige Bit wird gesetzt ), oder wir müssen nur den dynamischen Inhalt aktualisieren, um Änderungen zu erkennen (dies ist das Ziel der zweiten if ).

Wenn die Komponente zum ersten Mal angezeigt wird, erstellen wir alle Elemente. Wenn Änderungen erkannt werden, aktualisieren wir nur die Änderungen. ɵt interne Methode ɵt verantwortlich (beachten Sie den Kleinbuchstaben t ), die der von Angular exportierten Funktion textBinding entspricht:


Funktion textBinding

Der erste Parameter ist also der Index des zu aktualisierenden Elements, der zweite der Wert. In diesem Fall erstellen wir mit dem Befehl core.ɵT(2); ein leeres Textelement mit Index core.ɵT(2); . Es fungiert als Platzhalter für den name . Wir aktualisieren es mit dem Befehl core.ɵt(2, ctx.name); bei Erkennung einer Änderung der entsprechenden Variablen.

Im Moment zeigt die Ausgabe dieser Komponente weiterhin den Text Hello, Component , obwohl wir den Wert der Eigenschaft name ändern können, was zu einer Änderung des Textes auf dem Bildschirm führt.

Damit die Anwendung wirklich interaktiv wird, fügen wir hier ein Dateneingabefeld mit einem Ereignis-Listener hinzu, der die Komponentenmethode updateName() :

 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(); } // ... }, 

Die Ereignisbindung wird in der Zeile core.ɵL('input', $event => ctx.updateName($event.target.value)); . Die ɵL Methode ɵL verantwortlich, den Ereignis-Listener für das aktuellste der deklarierten Elemente ɵL . Das erste Argument ist der Name des Ereignisses (in diesem Fall ist die input das Ereignis, das ausgelöst wird, wenn sich der Inhalt des <input> -Elements ändert), das zweite Argument ist ein Rückruf. Dieser Rückruf akzeptiert Ereignisdaten als Argument. Dann extrahieren wir den aktuellen Wert aus dem Zielelement des Ereignisses, dh aus dem <input> -Element, und übergeben ihn an die Funktion in der Komponente.

Der obige Code entspricht dem Schreiben des folgenden HTML-Codes in eine Vorlage:

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

Jetzt können Sie den Inhalt des <input> -Elements bearbeiten und beobachten, wie sich der Text in der Komponente ändert. Das Eingabefeld wird jedoch beim Laden der Komponente nicht ausgefüllt. Damit alles auf diese Weise funktioniert, müssen Sie dem Vorlagenfunktionscode eine weitere Anweisung hinzufügen, die ausgeführt wird, wenn eine Änderung festgestellt wird:

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

Hier verwenden wir eine andere integrierte Methode des Renderingsystems, ɵp , die die Eigenschaft eines Elements mit einem bestimmten Index aktualisiert. In diesem Fall wird Index 4 an die Methode übergeben, bei der es sich um den Index handelt, der dem input zugewiesen ist, und wir ctx.name Methode an, den Wert ctx.name in die value Eigenschaft dieses Elements ctx.name .

Jetzt ist unser Beispiel endlich fertig. Wir haben die bidirektionale Datenbindung mithilfe der Ivy-Rendering-System-API von Grund auf implementiert. Das ist einfach toll.
Hier können Sie mit dem fertigen Code experimentieren.

Wir sind jetzt mit den meisten Grundbausteinen des neuen Ivy-Compilers vertraut. Wir wissen, wie man Elemente und Textknoten erstellt, Eigenschaften bindet und Ereignis-Listener konfiguriert und wie man das Änderungserkennungssystem verwendet.

Informationen zu den Blöcken * ngIf und * ngFor


Bevor wir die Ivy-Studie beenden, schauen wir uns ein anderes interessantes Thema an. Lassen Sie uns nämlich darüber sprechen, wie der Compiler mit Untermustern arbeitet. Dies sind die Muster, die für *ngIf oder *ngFor . Sie werden auf besondere Weise verarbeitet. Schauen wir uns an, wie Sie *ngIf in unserem hausgemachten Vorlagencode verwenden.

Zuerst müssen Sie das npm-Paket @angular/common *ngIf @angular/common installieren - hier wird *ngIf . Als nächstes müssen Sie die Direktive aus diesem Paket importieren:

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

Um NgIf in der Vorlage verwenden zu können, müssen Sie einige Metadaten bereitstellen, da das Modul @angular/common nicht mit Ivy kompiliert wurde (zumindest beim Schreiben des Materials, und dies wird sich in Zukunft wahrscheinlich ändern Einführung von ngcc ).

Wir werden die ɵdefineDirective Methode verwenden, die mit der bekannten ɵdefineComponent Methode zusammenhängt. Es definiert Metadaten für Direktiven:

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

Ich habe diese Definition im Angular-Quellcode zusammen mit der ngFor . NgIf wir NgIf für die Verwendung in Ivy vorbereitet haben, können wir der Liste der Anweisungen für die Komponente Folgendes hinzufügen:

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

Als Nächstes definieren wir das Untermuster nur für die durch *ngIf begrenzte *ngIf .

Angenommen, Sie müssen ein Bild anzeigen. Lassen Sie uns eine neue Funktion für diese Vorlage innerhalb der Vorlagenfunktion festlegen:

 function ifTemplate(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(); } } 

Diese Vorlagenfunktion unterscheidet sich nicht von der bereits geschriebenen. Es verwendet dieselben Konstrukte, um ein img Element innerhalb eines div Elements zu erstellen.

Und schließlich können wir alles zusammenfügen, indem ngIf der Komponentenvorlage die Direktive ngIf hinzufügen:

 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')); } function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {   // ... } }, 

Beachten Sie den Aufruf der neuen Methode am Anfang des Codes ( core.ɵC(5, ifTemplate, null, ['ngIf']); ). Es deklariert ein neues Containerelement, dh ein Element mit einer Vorlage. Das erste Argument ist der Index des Elements, wir haben solche Indizes bereits gesehen. Das zweite Argument ist die soeben definierte Untermusterfunktion. Es wird als Vorlage für das Containerelement verwendet. Der dritte Parameter ist der Tag-Name für das Element, was hier nicht sinnvoll ist, und schließlich gibt es eine Liste von Anweisungen und Attributen, die diesem Element zugeordnet sind. Hier kommt ngIf ins ngIf .

In der Zeile core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); Der Status des Elements wird aktualisiert, indem das Attribut ngIf an den Wert des logischen Ausdrucks ctx.name === 'Igor' . Dies prüft, ob die name Eigenschaft der Komponente gleich Igor .

Der obige Code entspricht dem folgenden HTML-Code:

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

Hier kann festgestellt werden, dass der neue Compiler nicht den kompaktesten Code erzeugt, aber im Vergleich zu dem, was er jetzt ist, nicht so schlecht ist.

Hier können Sie mit einem neuen Beispiel experimentieren. NgIf den Namen Igor in das Feld Your name , um den Abschnitt NgIf in Aktion zu sehen.

Zusammenfassung


Wir haben uns ziemlich viel mit den Funktionen des Ivy-Compilers beschäftigt. Hoffentlich hat diese Reise Ihr Interesse an einer weiteren Erkundung von Angular geweckt. Wenn ja, dann haben Sie jetzt alles, was Sie brauchen, um mit Ivy zu experimentieren. Jetzt wissen Sie, wie Sie Vorlagen in JavaScript „übersetzen“ und auf dieselben Winkelmechanismen zugreifen, die Ivy verwendet, ohne diesen Compiler zu verwenden. Ich nehme an, all dies gibt Ihnen die Möglichkeit, die neuen Winkelmechanismen so tief zu erkunden, wie Sie möchten.

Hier , hier und hier - drei Materialien, in denen Sie nützliche Informationen über Ivy finden. Und hier ist der Quellcode für Render3.

Liebe Leser! Wie denkst du über die neuen Funktionen von Ivy?

Source: https://habr.com/ru/post/de419995/


All Articles