mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 12:57:45 +00:00
Add explaination of Angular's template compiler
This commit is contained in:
@@ -87,3 +87,129 @@ The short answer is that Angular uses something called "Zone.js" to track all as
|
||||
> What does any of that mean? That's a lot of works that doesn't seem to mean very much if you're not already in the know.
|
||||
|
||||
I agree; so let's answer this better with a longer step-by-step explanation of how Angular does its rendering and reactivity using Zone.js.
|
||||
|
||||
This step-by-step explanation will have us explore:
|
||||
|
||||
<!-- // TODO: ADD LINKS -->
|
||||
|
||||
- How Angular's template compiler outputs functions that render contents
|
||||
- How Angular's templates are called in order to update contents on-screen
|
||||
- How Angular's change detection would work without Zone.js (and why it's a DX nightmare)
|
||||
- How Zone.js Monkey-patches async APIs to call change detection
|
||||
- How Angular has it's own internal instance of Zone.js called NgZone
|
||||
- Writing our own minimal version of Angular from scratch
|
||||
|
||||
|
||||
|
||||
# How Angular's template compiler works
|
||||
|
||||
Earlier last year, the Angular team published a blog post titled ["How the Angular Compiler Works"](https://blog.angular.io/how-the-angular-compiler-works-42111f9d2549). In it, they demonstrated how the `NGC` compiler takes the following code:
|
||||
|
||||
```typescript
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: '<span>Your name is {{name}}</span>',
|
||||
})
|
||||
export class AppCmp {
|
||||
name = 'Alex';
|
||||
}
|
||||
```
|
||||
|
||||
And outputs something like this:
|
||||
|
||||
```javascript
|
||||
import { Component } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
|
||||
export class AppCmp {
|
||||
constructor() {
|
||||
this.name = 'Alex';
|
||||
}
|
||||
}
|
||||
AppCmp.ɵfac = function AppCmp_Factory(t) { return new (t || AppCmp)(); };
|
||||
AppCmp.ɵcmp = i0.ɵɵdefineComponent({
|
||||
type: AppCmp,
|
||||
selectors: [["app-cmp"]],
|
||||
decls: 2,
|
||||
vars: 1,
|
||||
template: function AppCmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "span");
|
||||
i0.ɵɵtext(1);
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
i0.ɵɵadvance(1);
|
||||
i0.ɵɵtextInterpolate1("Your name is ", ctx.name, "");
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppCmp, [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app-cmp',
|
||||
template: '<span>Your name is {{name}}</span>',
|
||||
}]
|
||||
}], null, null); })();
|
||||
```
|
||||
|
||||
While their article goes more in-depth into how the compiler works (it's a great read!), let's keep our focus narrow for the intention of this article.
|
||||
|
||||
Namely, let's look at the `template` property function on the `ɵɵdefineComponent` function call.
|
||||
|
||||
```javascript
|
||||
template: function AppCmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "span");
|
||||
i0.ɵɵtext(1);
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
i0.ɵɵadvance(1);
|
||||
i0.ɵɵtextInterpolate1("Your name is ", ctx.name, "");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, we're receiving two arguments: `rf` (short for "render flags") and `ctx` (short for "context"). This function is called by Angular itself when the template is ready to either be rendered for the first time or updated afterwards.
|
||||
|
||||
Depending on how the template needs to be re-ran, the "render flag" (`rf`) will be passed differently, which allows Angular more control over how code is updated or not.
|
||||
|
||||
There are [only two flags that are currently defined in Angular 15](https://github.com/angular/angular/blob/a6849f27af129588091f635c6ae7a326241344fc/packages/core/src/render3/interfaces/definition.ts#LL50-L56C2):
|
||||
|
||||
```typescript
|
||||
// Source code from @angular/core
|
||||
// angular/packages/core/src/render3/interfaces/definition.ts
|
||||
|
||||
export const enum RenderFlags {
|
||||
/* Whether to run the creation block (e.g. create elements and directives) */
|
||||
Create = 0b01,
|
||||
|
||||
/* Whether to run the update block (e.g. refresh bindings) */
|
||||
Update = 0b10
|
||||
}
|
||||
```
|
||||
|
||||
The first render flag that will be passed to the `template` function is the `Create` flag, which will call the first `if` statement. Let's strip away everything but the first `if` statement:
|
||||
|
||||
```javascript
|
||||
i0.ɵɵelementStart(0, "span");
|
||||
i0.ɵɵtext(1);
|
||||
i0.ɵɵelementEnd();
|
||||
```
|
||||
|
||||
Here, very generally, Angular is saying: "create a `span` element, and mark it such that text should be placed within it".
|
||||
|
||||
After this is ran, Angular runs the `Update` render flag through the template compiler:
|
||||
|
||||
```javascript
|
||||
i0.ɵɵadvance(1);
|
||||
i0.ɵɵtextInterpolate1("Your name is ", ctx.name, "");
|
||||
```
|
||||
|
||||
Here, it's saying that we should interpolate the string `"Your name is Alex"` based on the property received from `ctx.name` and place it into the element's text area.
|
||||
|
||||
By having our template function have two distinct render phases, triggered by flags passed into the function, we're able to create the `span` on the first render and update the text values of the `span` on subsequent renders, without the need for re-initializing the `span` element each time we change the element's text.
|
||||
|
||||
Reference in New Issue
Block a user