---
title: "Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 `ɵɵ` 指令 - Angryecho"
description: "Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 ɵɵ 指令 系列：Angular 22 源码级深度解析 版本基线：angular@22.2.3（本仓库） 本章导读 第 1 章我们站在\"运行时\"这边，看 ɵcmp 和模板函数是怎么被消费的。"
canonical_url: "https://www.angryecho.com/c/qian_duan_she_qu/p/P2TBC4BObEO0cYfviSQnt"
meta:
  "og:description": "Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 ɵɵ 指令 系列：Angular 22 源码级深度解析 版本基线：angular@22.2.3（本仓库） 本章导读 第 1 章我们站在\"运行时\"这边，看 ɵcmp 和模板函数是怎么被消费的。"
  "og:image": "https://www.angryecho.com/_og/d/c_Post%2Ctitle_~5ZCQ5qe9572R%2Cdescription_~5ZCQ5qe95LiA5LiL77yM5bCx5LiK5ZCQ5qe9572R44CC%2CpostId_P2TBC4BObEO0cYfviSQnt%2Crevision_~MjAyNi0xMS0wM1QwOTowMDo0NC4zMzYxNzNa%2Cp_Ii9jL3FpYW5fZHVhbl9zaGVfcXUvcC9QMlRCQzRCT2JFTzBjWWZ2aVNRbnQi%2Cs_v8u8Fgwo7ZxW4kxg.png"
  "og:image:height": "630"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 `ɵɵ` 指令"
---

> Index of every page on this site: [llms.txt](https://www.angryecho.com/llms.txt)

# **Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 \`ɵɵ\` 指令**

## Angular 22 源码级深度解析-第 02 章 编译链路总览：从模板字符串到 `ɵɵ` 指令

> 系列：Angular 22 源码级深度解析 版本基线：`angular@22.2.3`（本仓库）

---

### 本章导读

第 1 章我们站在"运行时"这边，看 `ɵcmp` 和模板函数是怎么被消费的。这一章站到对面去——**编译器**——回答那个更根本的问题：你写在 `.html` 里的一行模板，到底是怎么变成一串 `ɵɵelementStart` 调用的？

这一章要打通一条完整的链路：

```
<span>Hello {{name}}!</span>          ← 你写的模板字符串
        │
        ▼  HTML 词法/语法分析 (ml_parser)
   HTML AST
        │
        ▼  R3 转换 + 绑定解析 (r3_template_transform / binding_parser)
   R3 AST（Angular 语义的 AST）
        │
        ▼  指令/管道匹配与类型检查 (t2_binder / typecheck)
   R3 AST + 依赖信息
        │
        ▼  ingest（吸入 IR）
   IR（中间表示：op 列表）
        │
        ▼  transform（74 个变换阶段，实际跑 57 个）
   优化后的 IR
        │
        ▼  emit（生成代码）
   function Cmp_Template(rf, ctx) {
     if (rf & 1) { ɵɵelementStart(0, 'span'); ɵɵtext(1); ɵɵelementEnd(); }
     if (rf & 2) { ɵɵadvance(); ɵɵtextInterpolate1('Hello ', ctx.name, '!'); }
   }
```

除了这条主线，本章还要解决三个绕不开的工程问题：

1. **为什么 Angular 的库（npm 包）不是发布编译好的代码，而是发布"半成品"？** → 这引出 **partial compilation**（部分编译）与 **linker**（链接器）；
2. **JIT 和 AOT 到底差在哪？** → 引出 `R3JitReflector` 与运行时编译；
3. `**decls: 2, vars: 2**`** 这两个数字究竟是谁算出来的？** → 引出 `allocateSlots` 和 `countVariables` 两个阶段（第 1 章只说了"编译器算的"，本章给出算法）。

读完这一章，你再看任何一个 Angular 组件的编译产物，都能在脑子里把它还原成模板。

---

### 2.1 三种编译形态：JIT、AOT 与 Partial Compilation

Angular 的编译不是"一种方式"，而是**三种**，分别服务于不同场景。理解它们的差别，是理解 Angular 整个构建体系的前提。

#### 2.1.1 JIT（Just-In-Time，即时编译）

**做什么**：在浏览器里、运行时，拿到组件类和模板字符串，现场编译出 `ɵcmp`。

**什么时候用**：开发/调试、在线 Playground、动态加载未编译组件的场景。

**代价**：

- 要把编译器 `@angular/compiler` 打包进应用（几百 KB）；
- 启动时要花时间编译；
- 因为模板是运行时才知道的，很多编译期检查（类型检查、未知属性告警）做不了或做得很晚。

JIT 的实现入口很小，`packages/compiler/src/render3/r3_jit.ts` 全文只有 33 行，核心是一个"符号解析器"：

```
export class R3JitReflector implements ExternalReferenceResolver {
  constructor(private context: {[key: string]: unknown}) {}

  resolveExternalReference(ref: o.ExternalReference): unknown {
    // 只处理 @angular/core 的引用
    if (ref.moduleName !== '@angular/core') {
      throw new Error(
        `Cannot resolve external reference to ${ref.moduleName}, only references to @angular/core are supported.`,
      );
    }
    if (!Object.hasOwn(this.context, ref.name!)) {
      throw new Error(`No value provided for @angular/core symbol '${ref.name!}'.`);
    }
    return this.context[ref.name!];
  }
}
```

这个类暴露了 JIT 的本质：编译器生成的代码里会有"外部引用"（比如 `ɵɵdefineComponent` 这个符号），在 AOT 下这些引用会被编译成真实的 `import` 语句；但在 JIT 下没有 `import` 可用，于是用一个 `context` 对象（通常就是 `@angular/core` 模块的导出集合）来**按需查表**解析这些符号。`R3JitReflector` 就是这个查表器。

第 1 章 §1.9.1 里 `bootstrapApplication` 在 JIT 模式下要 `await resolveJitResources()` 先拉取 `templateUrl`——因为 JIT 下模板是运行时才 fetch 的，编译必须在拿到模板文本之后才能开始。

#### 2.1.2 AOT（Ahead-Of-Time，预先编译）

**做什么**：在构建阶段（跑 `ng build` 时）就把模板编译成 `ɵcmp` 和模板函数，产物是纯 JS，不含编译器。

**什么时候用**：所有生产构建（Angular CLI 默认）。

**好处**：

- 不用打包编译器，体积小；
- 启动即渲染，首屏快；
- **编译期就能做完整类型检查和模板错误检查**——你在 IDE 里看到的"模板里 `user.nmae` 不存在"这类报错，就是 AOT 期间的类型检查给的。

这也是 Angular 默认走 AOT 的原因。本系列后面所有关于编译的讨论，默认都是 AOT。

#### 2.1.3 Partial Compilation（部分编译）+ Linker：给"库"准备的第三条路

前两条路解决了"应用怎么编译"。但\*\*库（npm 包）\*\*遇到一个死结。

假设你是组件库作者，用 Angular 写了个 `@my-lib/button`。你在发布时有两个选择：

| 选择 | 后果 |
| --- | --- |
| 发布**源码**（带 `@Component` 装饰器、模板字符串） | 每个使用者都要把 `@angular/compiler` 装进来自己编译；编译慢、体积大、版本耦合 |
| 发布**完全编译**的产物（含 `ɵɵelementStart` 等指令调用） | **灾难**：产物被绑死在编译它的那个 Angular 版本的内部实现上。用户用 Angular 22.2 编译、22.3 运行时，只要 22.3 改了某个 `ɵɵ` 指令的签名，库就崩了 |

第二个选项正是 View Engine（Angular 9 之前的老引擎）时代的惨痛教训——**库必须和使用它的应用用同一个 Angular 版本编译**，否则各种诡异崩溃。这被称为"库与应用的版本锁死"。

Angular 的解法是 **partial compilation（部分编译）+ linker（链接器）**：

1. **库作者**发布时，编译器把组件编译成一个\*\*"声明式元数据"的半成品\*\*——它保留了模板字符串、选择器、输入输出等**语义信息**，但不含任何 `ɵɵ` 指令调用；
2. **应用构建时**，一个叫 **linker** 的工具（通常是 Babel 插件）扫到这些半成品声明，用**当前应用所用的 Angular 版本**把它们"链接（link）"成最终的完整定义。

linker 的官方 README（`packages/compiler-cli/linker/README.md`）把这件事说得很清楚：

> The partial declaration format allows library packages to be published to npm **without exposing the underlying Ivy instructions**. （部分声明格式让库可以发布到 npm 而无需暴露底层的 Ivy 指令。）
>
> The tooling here allows application build tools (e.g. CLI) to produce **fully compiled components** at the point when the application is bundled. These linked files can be cached outside `node_modules` so it does not suffer from problems of mutating packages in `node_modules`.

两个关键点：

- **库不再包含任何** `**ɵɵ**`** 指令** → 库的产物与 Angular 内部实现解耦，只要"部分声明格式"不变，库就能被未来版本的 linker 链接；
- **链接发生在应用打包时**，产物可以缓存在 `node_modules` 之外 → 不用去改写 `node_modules` 里的文件（改写 `node_modules` 是构建工具的大忌：缓存失效、可移植性差）。

这套机制彻底解决了"库与应用版本锁死"。**Angular 库只需发布一次，能被后续任何 Angular 版本的应用消费**——只要那个版本的 linker 认识这个库用的声明版本。

为了做到这一点，部分声明里带了两个版本字段（`packages/compiler/src/render3/partial/api.ts`）：

```
/**
 * The minimum version of the compiler that can process this partial declaration.
 */
minVersion: string;

/**
 * ... will be able to detect which version a library is using and interpret its metadata accordingly.
 */
version: string;
```

- `version`：这个声明是用哪个编译器版本生成的；
- `minVersion`：处理它所需的**最低**编译器版本。

linker 用 semver 做版本范围匹配，选择对应版本的链接器（后面 §2.10 会看到 `PartialLinkerSelector` 的实现）。

#### 2.1.4 三种形态对照表

| 维度 | JIT | AOT | Partial + Linker |
| --- | --- | --- | --- |
| 编译时机 | 浏览器运行时 | 应用构建时 | 库构建时（部分）+ 应用构建时（链接） |
| 产物含 `ɵɵ` 指令 | 是（运行时生成） | 是 | **否**（库产物），链接后才有 |
| 需要打包编译器 | 需要 | 不需要 | 不需要（只需 linker，构建期） |
| 适用 | 开发调试、动态场景 | 生产应用 | **npm 库** |
| 版本耦合 | 强 | 强 | **弱**（靠声明格式 + semver 解耦） |

理解这张表，你就理解了 Angular 构建体系的"分人群策略"：**应用走 AOT 求快，库走 partial 求兼容，调试走 JIT 求方便。**

---

### 2.2 编译器全景：`packages/compiler` 的模块地图

编译器的源码在 `packages/compiler/src/`。它不是"一个编译器"，而是一条**分工明确的流水线**。先看地图：

```
packages/compiler/src/
├── ml_parser/              # ① HTML 层：词法分析 + 语法分析
│   ├── lexer.ts            #    词法：把字符流切成 token
│   ├── parser.ts           #    语法：把 token 组装成 HTML AST
│   ├── ast.ts              #    HTML AST 节点定义
│   ├── html_tags.ts        #    HTML 标签/属性的元数据（哪些是自闭合、哪些是 void 元素）
│   ├── tags.ts             #    Angular 特有的标签/属性定义（如 ng-container、ng-content）
│   ├── html_whitespaces.ts #    空白处理
│   ├── entities.ts         #    HTML 实体（&amp; 等）
│   └── xml_parser.ts       #    XML 模式（SVG/MathML 用）
│
├── expression_parser/      # ② 表达式层：解析 {{ }} 和 (click)="..." 里的 JS 表达式
│   ├── lexer.ts
│   ├── parser.ts
│   ├── ast.ts              #    表达式 AST（ASTWithSource、Binary、Conditional...）
│   └── serializer.ts
│
├── template_parser/        # ③ 模板层：绑定解析与预扫描
│   ├── binding_parser.ts   #    把属性解析成"属性绑定/事件绑定/指令输入"
│   └── template_preparser.ts #  预扫描（先快速判断有没有 ngIf 等结构指令）
│
├── render3/                # ④ Ivy 层：R3 AST 与代码生成
│   ├── r3_ast.ts           #    R3 AST 节点（Element、BoundText、BoundAttribute、DeferredBlock...）
│   ├── r3_template_transform.ts  #  HTML AST → R3 AST
│   ├── r3_identifiers.ts   #    运行时符号表（ɵɵelementStart 等的名字集中定义）
│   ├── r3_factory.ts       #    ɵfac 工厂生成
│   ├── r3_module_compiler.ts / r3_pipe_compiler.ts / r3_injector_compiler.ts
│   ├── r3_jit.ts           #    JIT 反射器（§2.1.1）
│   ├── partial/            #    ★ 部分编译：生成 ɵɵngDeclareComponent 等声明
│   └── view/               #    ★ 完整编译的主战场
│       ├── template.ts     #       parseTemplate()：解析模板的入口
│       ├── compiler.ts     #       compileComponentFromMetadata()：编译组件
│       ├── t2_binder.ts    #       指令/管道匹配（模板类型检查的基础）
│       ├── t2_api.ts       #       Target 抽象
│       ├── query_generation.ts
│       └── i18n/
│
├── template/pipeline/      # ★⑤ IR 流水线（Angular 17+ 的新编译器架构）
│   ├── ir/                 #    IR 节点与 trait 定义
│   └── src/
│       ├── ingest.ts       #    吸入：R3 AST → IR
│       ├── compilation.ts  #    CompilationJob / CompilationUnit 抽象
│       ├── emit.ts         #    发射：IR → 输出 AST（含 phases 顺序表）
│       ├── conversion.ts
│       ├── instruction.ts
│       └── phases/         #    ★ 74 个变换阶段
│
├── output/                 # ⑥ 输出层
│   ├── output_ast.ts       #    输出 AST（o.Expression / o.Statement ...）
│   ├── output_jit.ts       #    JIT 下执行输出 AST
│   ├── abstract_emitter.ts     #    发射器抽象基类（缩进 / 分号策略）
│   └── abstract_js_emitter.ts  #    JS 发射器实现
│
└── constant_pool.ts        # ★⑦ 常量池：把重复的字面量提升为共享常量
```

#### 2.2.1 这条流水线的设计哲学：分三层 AST

注意一个关键结构：**编译过程中 AST 换了三次形态**。

| 层 | AST 类型 | 谁产生的 | 表达的是什么 |
| --- | --- | --- | --- |
| HTML 层 | HTML AST（`ml_parser/ast.ts`） | `ml_parser` | 纯粹的 HTML 结构，不懂 Angular 语义 |
| Angular 层 | R3 AST（`render3/r3_ast.ts`） | `r3_template_transform` | 带 Angular 语义：`BoundText`、`BoundAttribute`、`IfBlock`、`DeferredBlock` |
| 输出层 | 输出 AST（`output/output_ast.ts`） | `template/pipeline` 的 emit | TypeScript/JS 代码本身（函数调用、if 语句、变量声明） |

为什么要分这么多层？因为**每一层要解决的问题不同**：

- HTML 层只关心"标签闭合了吗、属性写法合法吗"——它甚至可以是 XML 模式（SVG/MathML 用 `xml_parser.ts`）；
- R3 层关心"这是个插值吗、这是 `@if` 块吗、这个属性是 DOM 属性还是指令输入"——它需要 Angular 的领域知识；
- 输出层关心"生成什么样式的代码最优"——它已经不关心 HTML 了，只关心 JS 表达式和语句。

中间还夹着一层 **IR（中间表示）**，是 Angular 17 引入的新架构。IR 的作用是把"语义分析"和"代码生成"彻底分开：先 ingest 成 IR，然后**几十个独立的变换阶段**依次加工 IR，最后 emit 成输出 AST。这种"多趟（multi-pass）变换"的架构，让每个优化都是**独立、可测试、可增删**的小函数——这也是为什么 `phases/` 目录下能有 74 个文件。

#### 2.2.2 一个贯穿全局的角色：ConstantPool

`constant_pool.ts` 值得单独点名，因为它贯穿整条流水线。它的职责是：

> A constant pool allows a code emitter to **share constants** in an output context. The constant pool also supports **sharing access to ivy definitions references**.

简单说：编译器在生成代码时会产生大量字面量（选择器字符串、属性名数组、样式……），如果每个组件都内联一份，产物会又大又重复。`ConstantPool` 把它们收集起来，提升成**顶层的共享常量变量**，多个地方引用同一个变量。

它有几个设计细节很有意思：

```
const CONSTANT_PREFIX = '_c';   // 生成的常量变量名前缀，你会在产物里看到 _c0、_c1、_c2...

/**
 * Generally all primitive values are excluded from the `ConstantPool`, but there is an exclusion
 * for strings that reach a certain length threshold. This constant defines the length threshold for
 * strings.
 */
const POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS = 50;
```

注意这个"50"：**短字符串不值得提升**（`_c0 = 'div'` 反而比直接写 `'div'` 更占空间，还得付出一次变量查找），只有长度 ≥ 50 的字符串才进池。这是编译器里典型的"优化也要算账"——不是所有常量都该提升，得权衡。

还有那个 `FixupExpression` 类，它解决的是一个鸡生蛋问题：

```
/**
 * A node that is a place-holder that allows the node to be replaced when the actual
 * node is known.
 *
 * This allows the constant pool to change an expression from a direct reference to
 * a constant to a shared constant.
 */
class FixupExpression extends o.Expression {
  shared = false;
  // ...
  fixup(expression: o.Expression) {
    this.resolved = expression;
    this.shared = true;
  }
}
```

编译器在生成代码时，一开始不知道某个值最终会不会被共享（可能先内联了，后来发现还有别处也用了同一个值）。于是先发一个 `FixupExpression` 占位符，等所有代码都生成完、池子收集齐了，再回头 `fixup()` 把它替换成指向共享常量的引用。**"先占位、后回填"是编译器处理未知决策的经典手法。**

你在编译产物里看到的那些 `_c0`、`.consts` 数组，背后就是这套机制。

### 2.3 第一步：模板解析 —— 从字符串到 HTML AST

编译的第一步是把模板字符串变成结构化的树。这一步由 `ml_parser/`（"ml" = markup language）完成，它分两个经典阶段：**词法分析（lex）** 和 **语法分析（parse）**。

#### 2.3.1 词法分析：字符流 → Token 流

`ml_parser/lexer.ts` 负责把字符流切成有意义的 token。对 `<span class="x">Hello</span>`，它会产出：

```
TAG_OPEN_START  (<span)
ATTR_NAME       (class)
ATTR_QUOTE      (")
ATTR_VALUE_TEXT (x)
ATTR_QUOTE      (")
TAG_OPEN_END    (>)
TEXT            (Hello)
TAG_CLOSE       (</span>)
EOF
```

词法分析器是**状态机**驱动的，它要处理 HTML 的各种古怪之处：

- `<div>`、 `<br/>`、 `<input>`（void 元素，没有闭合标签）；
- 属性值可以单引号、双引号、无引号；
- 注释 `<!-- -->`、CDATA、DOCTYPE；
- HTML 实体 `&amp;` `&#123;`（ `entities.ts` 负责解码）；
- 插值 `{{...}}` 要作为特殊 token 识别出来（不能当普通文本）；
- 扩展操作符（控制流 `@if` 的 `@`、 `}`）也要识别。

有个细节体现了 Angular 的严谨：`ml_parser/html_tags.ts` 和 `tags.ts` 维护了**标签元数据表**——哪些标签是自闭合的（`img`、`br`、`input`），哪些属性是特殊的。这些元数据决定了解析器"看到一个 `<img>` 时，要不要等它的闭合标签"。HTML 的宽容性（浏览器能容错各种错误写法）在这套表里被显式建模了。

另外 `xml_parser.ts` 的存在说明：SVG 和 MathML 是 XML 方言，解析规则比 HTML 严格（比如 `<path />` 必须自闭合）。所以 Angular 在解析 SVG 内部的内容时会切到 XML 模式。**这就是为什么** `**<svg>**`** 内部的模板写法要和普通 HTML 略有不同**，根源在解析器模式切换。

#### 2.3.2 语法分析：Token 流 → HTML AST

`ml_parser/parser.ts` 消费 token 流，产出 HTML AST（`ml_parser/ast.ts`）。节点类型很朴素：

- `Element`（元素，含 `name`、 `attrs`、 `children`）
- `Text`（文本）
- `Comment`（注释）
- `Attribute`（属性）
- `Expansion`（插值 `{{...}}`，含 `ExpansionCase`，用于 i18n 的 ICU 表达式）
- `Block` 系列（控制流块，由 `@` 语法产生）

这一层**完全不懂 Angular 语义**。对它来说，`*ngIf="cond"`、`[title]="x"`、`@if (cond) {}` 都只是"带特殊名字的属性/块"而已。它只保证一件事：**树的结构是对的**（标签正确嵌套、闭合匹配）。

语法分析器还负责 `html_whitespaces.ts` 里的空白处理——默认情况下 Angular 会折叠模板里的多余空白（这就是为什么你可以在模板里随意缩进而不影响渲染）。有个 `preserveWhitespaces` 选项可以关掉这个行为（第 1 章 §1.18 提到它在 `ɵcmp` 里有个字段）。

#### 2.3.3 输出：一棵纯 HTML 树

对 `Hello {{name}}!` 这段模板，`ml_parser` 的产出是：

```
Element(span)
└── Text("Hello ")
└── Expansion / Interpolation: {{ name }}
└── Text("!")
```

注意此时 `{{name}}` 里的 `name` **还只是一个字符串**，没有被解析成表达式。表达式解析是下一层的事。这种"分层"是刻意的：HTML 解析器不该懂 JavaScript。

---

### 2.4 第二步：R3 AST —— 注入 Angular 语义

HTML AST 只是"结构正确"，还缺少 Angular 的灵魂：哪个属性是绑定？哪个是事件？`@if` 块的条件表达式是什么？这一步由 `render3/r3_template_transform.ts` 完成，产出 **R3 AST**（`render3/r3_ast.ts`）。

#### 2.4.1 R3 AST 的节点家族

`r3_ast.ts` 里定义的节点类型，就是 Angular 模板语义的完整清单：

**文本与元素**：

```
export class Text implements Node { ... }          // 静态文本
export class BoundText implements Node { ... }     // 含插值的文本：Hello {{name}}
export class TextAttribute implements Node { ... } // 静态属性：class="x"
export class Element implements Node { ... }       // 元素
export class Template implements Node { ... }      // 模板（<ng-template>）
```

**绑定**：

```
export class BoundAttribute implements Node { ... }  // 属性绑定：[title]="x" 或 指令输入
export class BoundEvent implements Node { ... }      // 事件绑定：(click)="do()"
```

**控制流块**（Angular 17+ 的新语法）：

```
export class IfBlock extends BlockNode implements Node { ... }        // @if
export class IfBlockBranch extends BlockNode implements Node { ... }  // @else if / @else
export class ForLoopBlock extends BlockNode implements Node { ... }   // @for
export class ForLoopBlockEmpty extends BlockNode implements Node { ... } // @empty
export class SwitchBlock extends BlockNode implements Node { ... }    // @switch
export class SwitchBlockCase extends BlockNode implements Node { ... }// @case
export class DeferredBlock extends BlockNode implements Node { ... }  // @defer
export class DeferredBlockPlaceholder ...   // @placeholder
export class DeferredBlockLoading ...       // @loading
export class DeferredBlockError ...         // @error
```

看到这份清单，你会发现一件重要的事：`**@if**`** /** `**@for**`** /** `**@switch**`** /** `**@defer**`** 在编译器的 AST 层面就是一等节点了**。而在老语法（`*ngIf`、`*ngFor`）时代，它们只是"碰巧叫 ngIf 的指令"，编译器要靠名字约定去识别。新语法把它们提升为**语言级构造**，带来的好处是：

- 编译器能针对性地优化（第 13 章讲 `@for` 的 reconciliation 时会看到）；
- 能做精确的类型检查；
- 不需要注册指令就能用；
- 能生成更紧凑的代码。

这也解释了第 1 章 §1.9 提到的那行：部分编译时，如果模板里出现了任何块（block），就要把 `minVersion` 设为 `17.0.0`——因为只有 17.0.0 以上的 linker 才认识这些新语法。

#### 2.4.2 表达式解析：`expression_parser`

R3 AST 里的绑定（`BoundAttribute`、`BoundEvent`、`BoundText`）都包含一个**表达式 AST**，由 `expression_parser/` 解析。

`{{ user.name + '!' }}` 会被解析成：

```
Binary(+)
├── PropertyRead(user.name)
│   ├── PropertyRead(user)
│   └── name
└── LiteralPrimitive('!')
```

表达式解析器是一个完整的 JavaScript 子集解析器——它处理：

- 二元运算（ `+ - * / && || ??`）、一元运算（ `! -`）；
- 三元条件 `a ? b : c`；
- 属性访问 `a.b`、 `a?.b`（安全导航）、 `a['b']`；
- 方法调用 `fn(x)`；
- 管道 `x | uppercase`；
- 特殊的 `$event`（事件对象）；
- 键值对象/数组字面量。

**它刻意不是完整的 JS 解析器**。Angular 模板表达式是 JS 的一个**受限子集**：不支持赋值、不支持 `new`、不支持语句、不支持函数声明。这不是偷懒，是**安全与性能的双重考量**：

- 模板表达式不能产生副作用（否则变更检测会失控）；
- 受限语法让类型检查（模板类型检查器）可以精确推断；
- 受限语法让代码生成更简单、更快。

`expression_parser/lexer.ts` 里还有一类特殊处理：`$event`、`$any()` 这些"魔法"标识符，以及管道符号 `|` 的优先级处理。

#### 2.4.3 绑定解析：`binding_parser.ts` —— 三选一的判定

`template_parser/binding_parser.ts` 是整个编译链路里最"裁决性"的一环。它要回答一个问题：**模板里的一个属性，到底是三种东西里的哪一种？**

以 `<div [title]="x" (click)="y" role="button">` 为例：

| 写法 | 判定结果 | 依据 |
| --- | --- | --- |
| `[title]="x"` | **属性绑定**（Property Binding） | 方括号语法 |
| `(click)="y"` | **事件绑定**（Event Binding） | 圆括号语法 |
| `role="button"` | **静态属性**（TextAttribute） | 无特殊语法 |
| `[dir.title]="x"` | **指令输入绑定** | 带指令前缀 |
| `[(ngModel)]="v"` | **双向绑定** | 香蕉盒语法 `[()]` |
| `animate.enter="..."` | **动画绑定** | 特殊前缀 |

这个判定不能只靠语法——还需要**知道这个元素上匹配了哪些指令**。因为 `[title]` 如果碰巧是某个指令的 `@Input() title`，它就应该被当作指令输入处理，而不是 DOM 属性。所以绑定解析需要和"指令匹配"配合，这正是下一节的主题。

`template_parser/template_preparser.ts` 则是一个**轻量预扫描器**：在做完整解析之前，先快速扫一遍看有没有结构性指令（如 `ngIf`、`ngFor`）或 `<ng-template>`，决定后续解析策略。这是编译器的常见优化——用一次廉价的扫描避免昂贵的完整解析。

---

### 2.5 第三步：指令匹配与类型检查

模板解析完了，编译器还不知道一个关键信息：`**<my-card>**`** 这个元素上，到底匹配了哪些指令？** 这决定了：

- `[title]` 是 DOM 属性还是某个指令的输入？
- 这个元素需要多少个 `LView` 槽位（EXPANDO 段，见第 1 章 §1.11）？
- 该元素上的 provider 有哪些？

这一步由 `render3/view/t2_binder.ts` 完成。

#### 2.5.1 `R3TargetBinder`：把模板和指令表对上

```
export function findMatchingDirectivesAndPipes(template: string, directiveSelectors: string[]) { ... }

export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetBinder<DirectiveT> { ... }
```

`R3TargetBinder` 的职责是：给定"这个组件可见的指令/管道集合"（来自它的 `imports`），遍历模板的元素，用**选择器匹配算法**（`render3/../directive_matching.ts`）找出每个元素上匹配的指令。

选择器匹配本身是个有意思的算法问题。Angular 的选择器支持：

```
my-card                  /* 元素选择器 */
[highlight]              /* 属性选择器 */
.highlight               /* class 选择器 */
my-card[highlight]       /* 组合 */
:not(.disabled)          /* 否定 */
a,b                      /* 列表 */
```

匹配器要把这些 CSS 风格的选择器和模板元素对上。它用的是**先否定、后正向**的策略：先排除掉所有 `:not(...)` 匹配失败的，再在剩下的里找最具体的。

#### 2.5.2 匹配结果如何流入后续编译

匹配完成后，每个元素上会挂上"匹配的指令列表"。这些信息会：

1. **决定绑定归属**： `[title]` 若命中某个指令的输入，就生成指令输入绑定（走 `ɵɵproperty` 的指令分支）；否则当 DOM 属性处理；
2. **决定是否需要指令匹配代码**：如果一个组件**完全没有指令依赖**（standalone 且 `imports` 为空），编译器可以切换到一种特殊模式——这一点极其重要，下一节详述；
3. **生成** `**dependencies**`** 数组**：组件定义里的 `dependencies: [Foo, Bar]` 就是这么来的；
4. **支撑模板类型检查**：IDE 里的模板报错，靠的就是"这个元素匹配了哪个指令 → 那个指令有哪些输入 → 你传的类型对不对"这条链。

#### 2.5.3 顺带说清：模板类型检查是怎么做到的

Angular 的模板类型检查（你在 IDE 里看到的"Property 'nmae' does not exist"）不是编译器前端做的，而是一个巧妙的\*\*"用 TypeScript 检查 TS 代码"的技巧\*\*：

编译器把你的模板**反向生成成一段 TypeScript 代码**（Type Check Block，TCB），比如把 `{{user.nmae}}` 生成为：

```
function tcb(ctx: MyComponent) {
  // 编译器生成的检查代码
  _t1 = ctx.user.nmae;   // TypeScript 会在这里报错：Property 'nmae' does not exist
}
```

然后交给 TypeScript 编译器去检查这段生成的代码。TS 报错了，Angular 就把错误位置映射回模板的对应行列。**用现成的类型系统检查 DSL，而不是自己实现一个类型检查器**——这是个非常聪明的工程决策，代价小、准确度高、还能随 TS 升级自动受益。

这段逻辑在 `compiler/src/template/typecheck/` 和 `compiler-cli/src/ngtsc/typecheck/` 里。本系列不单开一章讲它，但你至少要知道：**模板里的红线，本质是 TypeScript 在检查编译器生成的"影子代码"。**

---

### 2.6 第四步：IR 流水线 —— Angular 17+ 的编译器架构

从这里开始进入本章最硬核的部分。Angular 17 重写了编译器的代码生成部分，引入了 **IR（Intermediate Representation，中间表示）+ 多趟变换**的架构。源码在 `packages/compiler/src/template/pipeline/`。

#### 2.6.1 为什么需要 IR

在 IR 之前，编译器是"一遍扫描直接生成代码"的：遍历 AST 的同时就吐出 `ɵɵxxx` 调用。这种做法的问题是**优化无从下手**：

- 你想做"把相邻的 `advance` 合并"，但等你发现可以合并时，前一条指令已经生成了；
- 你想做"常量提升"，但生成指令时还不知道后面会不会有相同常量；
- 你想做"重排指令顺序以复用变量"，但代码顺序已经定了。

IR 架构的解法是：**把"生成"推迟到最后，中间用几十趟独立的变换来优化**。

```
R3 AST
  │  ingest（吸入）
  ▼
IR（一组 op 的列表，可被任意重排、合并、改写）
  │  transform（57 趟变换，每趟只做一件事）
  ▼
优化后的 IR
  │  emit（发射）
  ▼
输出 AST → TypeScript 代码
```

每一趟变换都是一个**纯函数**（接收 `job`，就地修改），独立可测试。`phases/` 目录下有 **74 个文件**——这就是这套架构的"优化清单"。

#### 2.6.2 三种编译任务：`CompilationJobKind`

```
// template/pipeline/src/compilation.ts
export enum CompilationJobKind {
  Tmpl,    // 模板编译（组件的模板）
  Host,    // 宿主绑定编译（@HostBinding、host: {} 里的绑定）
  Both,    // 特殊值：表示"两种任务都适用"的阶段
}
```

一个组件的编译实际上包含**两个**编译任务：

1. **Tmpl**：组件模板本身的编译 → 生成 `ɵcmp.template` 函数；
2. **Host**：组件宿主元素上的绑定（ `@HostBinding('class.x')` 或 `host: {'[class.x]': 'expr'}`）→ 生成 `ɵcmp.hostBindings` 函数。

两者共用同一套 IR 设施，但阶段清单不同：`transform(job, kind)` 会只跑标注为该 `kind`（或 `Both`）的阶段。

#### 2.6.3 两种编译模式：`TemplateCompilationMode`

这是一个能显著提升 standalone 组件性能的优化，值得细看：

```
export enum TemplateCompilationMode {
  /** Supports the full instruction set, including directives. */
  Full,

  /** Uses a narrower instruction set that doesn't support directives and allows optimizations. */
  DomOnly,
}
```

判定逻辑在 `compileComponentFromMetadata` 里：

```
const compilationMode =
  meta.isStandalone && !meta.hasDirectiveDependencies
    ? TemplateCompilationMode.DomOnly
    : TemplateCompilationMode.Full;
```

**如果一个组件是 standalone 且没有任何指令依赖（没有** `**imports**`** 指令、模板里不依赖任何指令），就用** `**DomOnly**`** 模式编译。** 这种模式下：

- 生成的指令集更窄（不需要匹配指令、不需要 EXPANDO 段的指令实例化逻辑）；
- 可以做更多优化（因为没有"可能有指令会来改这个元素"的顾虑）。

这带来的实战启示很直接：**standalone 组件如果不用指令，模板编译产物会更小更快**。而绝大多数"纯展示型"组件（只有 HTML + 插值）恰好属于这一类。这是 Angular 推动 standalone 的又一份红利——不只是写法简化，连编译产物都更优。

#### 2.6.4 主流程源码：`compileComponentFromMetadata`

现在看完整编译组件的函数本体（`render3/view/compiler.ts`）。这个函数不长，却是整个编译链路的"总装车间"：

```
export function compileComponentFromMetadata(
  meta: R3ComponentMetadata<R3TemplateDependency>,
  constantPool: ConstantPool,
  bindingParser: BindingParser,
): R3CompiledExpression {
  const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
  addFeatures(definitionMap, meta);

  // e.g. `template: function MyComponent_Template(_ctx, _cm) {...}`
  const templateTypeName = meta.name;

  // ① @defer 依赖函数的处理（整组件级别）
  let allDeferrableDepsFn: o.ReadVarExpr | null = null;
  if (meta.defer.mode === DeferBlockDepsEmitMode.PerComponent && meta.defer.dependenciesFn !== null) {
    const fnName = `${templateTypeName}_DeferFn`;
    constantPool.statements.push(
      new o.DeclareVarStmt(fnName, meta.defer.dependenciesFn, undefined, o.StmtModifier.Final),
    );
    allDeferrableDepsFn = o.variable(fnName);
  }

  // ② 决定编译模式（DomOnly vs Full，见 §2.6.3）
  const compilationMode =
    meta.isStandalone && !meta.hasDirectiveDependencies
      ? TemplateCompilationMode.DomOnly
      : TemplateCompilationMode.Full;

  // ③ First the template is ingested into IR:
  const tpl = ingestComponent(
    meta.name, meta.template.nodes, constantPool, compilationMode,
    meta.relativeContextFilePath, meta.i18nUseExternalIds, meta.defer,
    allDeferrableDepsFn, meta.relativeTemplatePath,
    meta.enableTemplateSourceLocations || ENABLE_TEMPLATE_SOURCE_LOCATIONS,
    meta.legacyOptionalChaining, meta.foreignImports,
  );

  // ④ Then the IR is transformed to prepare it for code generation.
  transform(tpl, CompilationJobKind.Tmpl);

  // ⑤ Finally we emit the template function:
  const templateFn = emitTemplateFn(tpl, constantPool);

  // ⑥ 把结果写进定义对象
  if (tpl.contentSelectors !== null) {
    definitionMap.set('ngContentSelectors', tpl.contentSelectors);
  }
  definitionMap.set('decls', o.literal(tpl.root.decls as number));   // ★ decls 在这里定下来
  definitionMap.set('vars', o.literal(tpl.root.vars as number));     // ★ vars 在这里定下来
  if (tpl.consts.length > 0) {
    // 常量数组：如果有初始化器就包成箭头函数（惰性），否则直接字面量数组
    if (tpl.constsInitializers.length > 0) {
      definitionMap.set('consts', o.arrowFn([],
        [...tpl.constsInitializers, new o.ReturnStatement(o.literalArr(tpl.consts))],
        o.DYNAMIC_TYPE));
    } else {
      definitionMap.set('consts', o.literalArr(tpl.consts));
    }
  }
  definitionMap.set('template', templateFn);
  // ...
}
```

短短几十行，把三个阶段（ingest / transform / emit）串成一条清晰的主线。注意那句注释 `// First the template is ingested into IR` / `// Then the IR is transformed...` / `// Finally we emit...`——**编译器作者把架构写在注释里了**，读源码时这些注释是最好的路标。

另外注意 ⑥ 里 `consts` 的两种发射形态：如果常量里有"需要初始化器"的（比如要 `new` 一个对象），就把整个 `consts` 包进一个**箭头函数**（惰性求值，首次访问时才建）；否则直接是字面量数组（更快）。这又是一次"编译期决策"——带初始化器的东西不能静态求值，那就延迟到运行时。

---

### 2.7 变换阶段详解：57 趟变换都在干什么

`transform()` 的实现极简（`emit.ts`）：

```
export function transform(job: CompilationJob, kind: Kind): void {
  for (const phase of phases) {
    if (phase.kind === kind || phase.kind === Kind.Both) {
      phase.fn(job as CompilationJob & ComponentCompilationJob & HostBindingCompilationJob);
    }
  }
}
```

就是一个 for 循环，按数组顺序执行每个阶段。**真正的复杂度藏在** `**phases**`** 数组的顺序和内容里**。下面我把这 57 个（对 `Tmpl` 而言）阶段按"它们在干什么"分组讲一遍。这部分是本章最有价值的内容之一——它告诉你 Angular 编译器究竟做了哪些优化。

#### 2.7.1 第一组：语义规范化（把特殊语法降到统一的 IR）

```
resolveForeignContent          // 处理跨组件边界的内容
removeContentSelectors         // 移除不需要的 content selector
optimizeRegularExpressions     // 正则优化
emitNamespaceChanges           // SVG/MathML 命名空间切换（ɵɵnamespaceSVG 等）
propagateI18nBlocks            // i18n 块传播
wrapI18nIcus                   // i18n ICU 表达式包装
deduplicateTextBindings        // ★ 去重：相同的文本绑定只留一份
specializeStyleBindings        // ★ 样式绑定特化（[style.color] → 专用指令）
specializeBindings             // ★ 绑定特化（通用绑定 → 更专用的指令）
specializeControlProperties    // 控制流属性特化
convertAnimations              // 动画绑定转换
extractAttributes              // 属性提取（静态属性 → consts 数组）
createI18nContexts             // 创建 i18n 上下文
parseExtractedStyles           // 解析提取出的样式
removeEmptyBindings            // 移除空绑定
collapseSingletonInterpolations// ★ 单插值折叠：{{a}} → ɵɵtextInterpolate1
```

这一组的核心工作是**把高层语法降级（lower）成低层、更专用的形式**。最有代表性的是 `collapseSingletonInterpolations`——这正是第 1 章 §1.10.3 讲的：模板里 `Hello {{name}}!` 这种"只有一个插值"的文本，会被折叠成专用的 `ɵɵtextInterpolate1`，而不是通用的插值处理函数。

同理 `specializeBindings` 会把通用的属性绑定，根据类型（DOM 属性 / 指令输入 / class / style / 动画）替换成更专用的指令调用。**"特化"是编译器优化的经典手法：通用的慢路径留给少数情况，常见的快路径专门生成。**

#### 2.7.2 第二组：控制流与结构

```
orderOps                       // 重排 op 顺序（为后续优化铺路）
generateConditionalExpressions // @if/@switch → 条件表达式
generateBoundaryConditions     // 边界条件（@for 的边界等）
createPipes                    // 管道实例化（ɵɵpipe）
configureDeferInstructions     // @defer 指令配置
insertIncrementalHydrationRuntime  // ★ 增量水合运行时插入
createVariadicPipes            // 可变参数管道
generateArrowFunctions         // 需要惰性的地方生成箭头函数
generatePureLiteralStructures  // ★ 纯字面量结构（直接内联，不做纯函数调用）
generateProjectionDefs         // 内容投影定义（ɵɵprojectionDef）
generateLocalLetReferences     // @let 局部引用
generateVariables              // 变量生成
resolveBoundaries              // 解析视图边界
saveAndRestoreView             // ★ 保存/恢复视图上下文（嵌套视图切换时）
```

注意 `orderOps` 这个阶段排在很前面——它先把 op 排好序，后续阶段才能做"合并相邻的 advance"这类依赖顺序的优化。**编译器里阶段顺序本身就是设计的一部分。**

`generatePureLiteralStructures` 是又一个"算账"的优化：如果一个管道的参数全是字面量（`{{ 'x' | uppercase }}`），那它永远不变，直接内联结果，连纯函数调用都省了。

#### 2.7.3 第三组：名称与上下文解析

```
deleteAnyCasts                 // 删除 $any() 的类型转换（运行时不需要）
removeSafeNavigationMigration  // 安全导航迁移处理
resolveDollarEvent             // ★ $event 解析
generateTrackVariables         // @for 的 track 变量
removeIllegalLetReferences     // 非法 @let 引用检查
resolveNames                   // ★ 名称解析（局部变量、上下文引用 → 具体的读取表达式）
resolveDeferTargetNames        // @defer 目标名称
transformTwoWayBindingSet      // 双向绑定的 set 变换
optimizeTrackFns               // @for track 函数优化
resolveContexts                // ★ 上下文解析（ctx.x 还是 $event.x → 生成正确的读取路径）
resolveSanitizers              // ★ 消毒器解析（哪些绑定需要 XSS 消毒）
liftLocalRefs                  // 本地引用提升
expandSafeReads                // 安全读取展开（a?.b → a == null ? null : a.b）
stripNonrequiredParentheses    // 去掉多余括号（产物更好看、解析更快）
```

`resolveContexts` 是理解模板函数的关键：模板里写 `{{name}}`，但组件可能嵌套在 `@for` 里，此时 `name` 可能来自 `ctx` 也可能来自 for 循环的局部变量。这个阶段决定**生成** `**ctx.name**`** 还是** `**ɵɵreadContextLet(...)**`** 之类**。

`resolveSanitizers` 则是安全相关的：Angular 会自动识别哪些绑定可能引入 XSS（如 `[innerHTML]`），并插入消毒调用。这是 Angular "默认安全"的保障之一（第 12 章会展开）。

#### 2.7.4 第四组：i18n 专用

```
convertI18nText                // i18n 文本转换
convertI18nBindings            // i18n 绑定转换
removeUnusedI18nAttributesOps  // 移除未使用的 i18n 属性
assignI18nSlotDependencies     // i18n 槽位依赖
applyI18nExpressions           // 应用 i18n 表达式
resolveI18nElementPlaceholders   // i18n 元素占位符
resolveI18nExpressionPlaceholders // i18n 表达式占位符
extractI18nMessages            // ★ 提取 i18n 消息（供翻译）
collectI18nConsts              // i18n 常量收集
resolveI18nAttrSanitizers      // i18n 属性消毒
```

i18n 占了 9 个阶段，可见其复杂度。第 19 章会专门讲。

#### 2.7.5 第五组：★ 槽位与变量计数（`decls` / `vars` 的来源）

这一组是第 1 章留下的悬念的答案。

```
// template/pipeline/src/phases/slot_allocation.ts
export function allocateSlots(job: ComponentCompilationJob): void {
  // Process all views in the component and assign slot indexes.
  for (const unit of job.units) {
    // Slot indices start at 0 for each view (and are not unique between views).
    let slotCount = 0;

    for (const op of unit.create) {
      // Only consider declarations which consume data slots.
      if (!ir.hasConsumesSlotTrait(op)) {
        continue;
      }
      // Assign slots to this declaration starting at the current `slotCount`.
      op.handle.slot = slotCount;
      // Each declaration may use more than 1 slot, so increment `slotCount` to reserve the number
      // of slots required.
      slotCount += op.numSlotsUsed;
    }
    // Record the total number of slots used on the view itself...
  }
}
```

这就是 `decls` 的算法：

1. 遍历视图的 **create 阶段**的所有 op；
2. 只考虑"消耗槽位"的 op（即会创建 DOM 节点/管道/本地引用的那些，实现了 `ConsumesSlotOpTrait`）；
3. 给它们**从 0 开始顺序分配槽位号**（ `op.handle.slot = slotCount`）；
4. 每个 op 可能占多个槽位（ `numSlotsUsed`），累加；
5. 最终 `slotCount` 就是这个视图的 `decls`。

**"每个视图的槽位号都从 0 开始，且视图之间不唯一"**——这句注释很重要：第 1 章 §1.11.2 说 `LView` 的索引是"模板内的逻辑索引"，正是因为每个视图（根视图 + 每个嵌入式视图）都独立从 0 编号。这也是为什么 `ɵɵelementStart(0, ...)` 里的 `0` 在 `@if` 块内外都可能出现在不同的视图里。

```
// template/pipeline/src/phases/var_counting.ts
export function countVariables(job: CompilationJob): void {
  // First, count the vars used in each view, and update the view-level counter.
  for (const unit of job.units) {
    let varCount = 0;
    // Count variables on top-level ops first. Don't explore nested expressions just yet.
    for (const op of unit.ops()) {
      if (ir.hasConsumesVarsTrait(op)) {
        varCount += varsUsedByOp(op);
      }
    }
    // Count variables on expressions inside ops. We do this later because some of these expressions
    // might be conditional (e.g. `pipeBinding` inside of a ternary), and we don't want to interfere
    // with indices for top-level binding slots (e.g. `property`).
    // ...
  }
}
```

这是 `vars` 的算法，注意它的**两趟计数**（注释解释得很清楚）：

1. **先算顶层 op 的变量**：确保顶层绑定的槽位号连续、可预测；
2. **再算表达式内部的变量**：因为有些表达式是条件性的（比如三元表达式里的管道绑定），如果混在一起算，会打乱顶层绑定的索引分配。

**"先确定主干、再填充细节"**——这种顺序保证了最常用、最热的绑定拿到最紧凑的槽位。

#### 2.7.6 第六组：★ 生成 `ɵɵadvance`

```
// phases/generate_advance.ts（排在 countVariables 之后）
generateAdvance
```

第 1 章 §1.10.3 说 `ɵɵadvance()` 是"游标前进"指令。它就是在这一趟生成的：编译器遍历 update 阶段的 op，发现"当前 op 操作的节点，和上一个 op 不是同一个"时，就在两者之间插入一条 `ɵɵadvance()`。

**注意它的执行顺序必须在** `**countVariables**`** 之后**——因为要先知道每个 op 对应哪个槽位，才能判断"是否需要 advance"。阶段顺序的这种依赖关系，在 74 个阶段里到处都是，也是这套架构最难维护的地方（改一个阶段的顺序可能悄悄破坏另一个阶段的假设）。

#### 2.7.7 第七组：收尾与发射准备

```
nameFunctionsAndVariables      // 给函数和变量命名（Cmp_Template、_r0 等）
resolveDeferDepsFns            // @defer 依赖函数解析
mergeNextContextExpressions    // ★ 合并连续的 nextContext（ɵɵnextContext(2) 而不是两次 1）
generateNgContainerOps         // ng-container 处理（不需要真实 DOM）
collapseEmptyInstructions      // 折叠空指令
attachSourceLocations          // ★ 附加源码位置（供 sourcemap / 调试）
disableBindings                // 禁用绑定（@defer 等场景）
extractPureFunctions           // ★ 纯函数提取（管道 → ɵɵpureFunctionX）
reify                          // ★ 实体化：把 IR op 变成真正的指令调用表达式
chain                          // ★ 链式优化：ɵɵproperty 链式调用
```

三个值得展开：

`**mergeNextContextExpressions**`：模板里 `{{a.b.c}}` 如果 `a` 在外层视图，可能需要两次 `ɵɵnextContext(1)`。这个阶段把它们合并成一次 `ɵɵnextContext(2)`。**少一次函数调用 = 更快。**

`**reify**`**（实体化）**：这是 IR → 指令调用的关键一步。在此之前，IR 里的 op 还是"抽象操作"（"我要绑定 title 属性"）；`reify` 把它们变成**具体的** `**ɵɵproperty('title', ctx.x)**`** 调用表达式**。名字 "reify"（使抽象事物具体化）起得很贴切。

`**chain**`**（链式）**：这是产物里最显眼的优化之一。看这段产物：

```
ɵɵproperty("title", ctx.a)("id", ctx.b)("class", ctx.c);
```

注意是**链式调用**（一个函数调用后面直接跟括号），而不是三次独立的 `ɵɵproperty(...)` 语句。`chain` 阶段把连续的同类指令调用改成链式，好处是：

- 产物更小（少写几次 `ɵɵproperty`）；
- V8 对这种"单态调用点"的优化更友好。

**这就是为什么 Angular 的编译产物看起来那么"压缩"**——它是被 57 趟优化反复打磨过的。

#### 2.7.8 阶段顺序的"潜规则"

把这 57 趟按顺序排完，你会发现一条清晰的脉络：

```
语义规范化 → 控制流结构化 → 名称/上下文解析 → i18n → 槽位分配 → advance 生成 → 命名 → 收尾优化 → 实体化 → 链式
```

**先做语义转换，再做布局决策（槽位/变量），最后做代码形态优化。** 这个顺序不是随意的：

- 槽位必须在所有节点确定之后才能分配（所以 `allocateSlots` 排在中后段）；
- `advance` 必须在槽位之后（依赖槽位号判断）；
- `reify`/ `chain` 必须在最后（它们是"落地成代码"的动作，之后就没法再优化 IR 了）。

理解这条脉络，你就能预判"如果我要加一个新优化，应该插在哪一趟"。这也是读编译器源码时最有价值的收获之一——**不是记住 74 个文件名，而是理解它们的编排逻辑**。

### 2.8 发射阶段：两阶段函数是怎么被"拼"出来的

IR 优化完了，最后一步是把它变成真正的 TypeScript/JS 代码。这一步在 `template/pipeline/src/emit.ts`。

#### 2.8.1 `emitTemplateFn`：一个组件 = 一个根函数 + N 个子视图函数

```
export function emitTemplateFn(tpl: ComponentCompilationJob, pool: ConstantPool): o.FunctionExpr {
  const rootFn = emitView(tpl.root);          // 根视图函数
  emitChildViews(tpl.root, pool);             // 递归发射子视图
  return rootFn;
}

function emitChildViews(parent: ViewCompilationUnit, pool: ConstantPool): void {
  for (const unit of parent.job.units) {
    if (unit.parent !== parent.xref) continue;
    // Child views are emitted depth-first.
    emitChildViews(unit, pool);
    const viewFn = emitView(unit);
    pool.statements.push(viewFn.toDeclStmt(viewFn.name!));   // 子视图函数放进常量池的 statements
  }
}
```

**这解释了编译产物里一个常见现象**：如果你在模板里用了 `@if`，产物里除了 `Cmp_Template` 主函数，还会多出 `Cmp_Conditional_0_Template` 之类的额外函数。那些就是**嵌入式视图（embedded view）的函数**，在这里被作为独立的语句放进 `ConstantPool.statements`，最终输出到模块顶层。

顺序是**深度优先**：先递归发射子视图的子视图，再发射自己。这保证了函数声明的顺序符合"被引用前已定义"的要求（至少在大多数情况下）。

#### 2.8.2 `emitView`：把 create/update 两个列表装进 if 块

```
function emitView(view: ViewCompilationUnit): o.FunctionExpr {
  if (view.fnName === null) {
    throw new Error(`AssertionError: view ${view.xref} is unnamed`);
  }

  // 收集 create 阶段的语句
  const createStatements: o.Statement[] = [];
  for (const op of view.create) {
    if (op.kind !== ir.OpKind.Statement) {
      throw new Error(
        `AssertionError: expected all create ops to have been compiled, but got ${ir.OpKind[op.kind]}`,
      );
    }
    createStatements.push(op.statement);
  }

  // 收集 update 阶段的语句
  const updateStatements: o.Statement[] = [];
  for (const op of view.update) {
    if (op.kind !== ir.OpKind.Statement) { /* 同上断言 */ }
    updateStatements.push(op.statement);
  }

  // ★ 生成两个 if 块
  const createCond = maybeGenerateRfBlock(1, createStatements);
  const updateCond = maybeGenerateRfBlock(2, updateStatements);

  return o.fn(
    [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, o.DYNAMIC_TYPE)],
    [...createCond, ...updateCond],
    o.DYNAMIC_TYPE,
    undefined,
    view.fnName,
  );
}
```

这段代码把第 1 章 §1.10.3 的"两阶段模型"落实成了真实的函数结构。三个细节：

**① 断言"所有 op 都已被编译"。** `if (op.kind !== ir.OpKind.Statement) throw ...`——经过 `reify` 阶段后，每个 op 都应该已经变成 `Statement` 形态（真正可发射的代码）。如果还有 op 停留在抽象形态，说明**某个阶段漏了工作**，直接抛错而不是生成错误代码。这是编译器健壮性的体现：**宁可崩溃，也不静默产出错误代码。**

**② 两个** `**maybeGenerateRfBlock(1, ...)**`** 和** `**(2, ...)**`。看它的实现：

```
function maybeGenerateRfBlock(flag: number, statements: o.Statement[]): o.Statement[] {
  if (statements.length === 0) {
    return [];   // ★ 空的就不生成 if 块
  }
  return [
    o.ifStmt(
      new o.BinaryOperatorExpr(
        o.BinaryOperator.BitwiseAnd,
        o.variable(RENDER_FLAGS),
        o.literal(flag),
      ),
      statements,
    ),
  ];
}
```

这就生成了 `if (rf & 1) {...}` 和 `if (rf & 2) {...}`。注意那个 `statements.length === 0` 的短路：**如果某一段是空的（比如组件没有任何绑定，update 段为空），就完全不生成对应的 if 块**。产物里就只剩 `if (rf & 1) {...}`。这又是一次"编译期决策省运行时开销"——**不需要的分支一个字节都不产出。**

**③ 函数签名** `**(rf, ctx)**`。第 1 章看到的模板函数签名就是在这里定的：`RENDER_FLAGS`（数字类型）和 `CONTEXT_NAME`（动态类型）。

#### 2.8.3 从 IR 到产物的完整对照

把这一节和前面串起来，一个 `@if` + 插值的模板：

```
<div>Hello {{name}}!</div>
@if (show()) {
  <p>Extra</p>
}
```

大致会经历：

| 阶段 | 形态 |
| --- | --- |
| R3 AST | `Element(div)` 含 `BoundText`；`IfBlock` 含 `Element(p)` |
| ingest 后 IR | create: \[ElementStartOp, TextOp, ElementEndOp, TemplateOp(条件)]；update: \[InterpolateOp, ConditionalOp] |
| allocateSlots 后 | 根视图 decls=2（div + text）；条件视图 decls=1（p） |
| countVariables 后 | 根视图 vars=1（插值） |
| generateAdvance 后 | update 段插入 `ɵɵadvance(1)` |
| naming 后 | 主函数 `Cmp_Template`，子视图 `Cmp_Conditional_0_Template` |
| reify + chain 后 | 具体指令调用，链式合并 |
| emit 后 | `function Cmp_Template(rf, ctx) { if (rf & 1) {...} if (rf & 2) {...} }` + 顶层 `Cmp_Conditional_0_Template` 函数 |

这就是从模板到产物的完整旅程。

---

### 2.9 常量池深入：产物里那些 `_c0` 是怎么来的

第 1 章的读者可能已经在产物里见过这种代码：

```
const _c0 = ["title"];
const _c1 = (a) => ({title: a});
// ...
ɵɵdefineComponent({
  consts: [_c0],
  template: function Cmp_Template(rf, ctx) { ... }
});
```

这些 `_c0`、`_c1` 就是 `ConstantPool` 的产物。本节讲它的工作机制。

#### 2.9.1 池子的三个数据结构

```
export class ConstantPool {
  statements: o.Statement[] = [];                    // 最终输出的顶层语句（常量声明）
  private literals = new Map<string, FixupExpression>();   // 字面量 → 占位表达式
  private sharedConstants = new Map<string, o.Expression>(); // 已共享的常量
  // ...
}
```

工作流程：

1. 编译器某处需要一个常量（比如属性名数组 `['title']`）；
2. 调用 `constantPool.getConstLiteral(expr)`；
3. 池子给这个表达式**生成一个 key**（用 `KEY_CONTEXT` 遍历表达式的"原始形态"而非引用变量，保证相同结构得到相同 key）；
4. 如果 key 已存在 → 返回已有的共享引用（**去重**）；
5. 如果不存在 → 创建 `FixupExpression` 占位，登记进 `literals`；
6. 最后 `produceStatements()` 时，把所有收集到的常量变成顶层的 `const _cN = ...` 声明，并回填 `FixupExpression` 的引用。

#### 2.9.2 为什么需要 `FixupExpression`（先占位后回填）

回顾 §2.2.2 提到的那个类。它解决的是一个真实的时序问题：

编译器生成代码是**一遍流式**的。当它写下第一个 `['title']` 时，它不知道：

- 后面会不会还有一模一样的 `['title']`？（如果只有一个，内联反而更好）
- 这个常量最终会被放在池子的第几号？（ `_c0` 还是 `_c7`？）

所以它先发一个"占位符"（`FixupExpression`），把所有需要这个常量的地方都指向同一个占位对象。等全部生成完毕，池子知道"总共有 N 个常量、这个排第几、是否被多处共享"，再统一 `fixup()` 把占位符替换成真正的 `_cN` 引用。

**用一次延迟绑定，换取了全局最优决策。** 这和链接器（linker）的"符号重定位"思想其实是同一类问题的两种解法。

#### 2.9.3 那个"50"的阈值

```
const POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS = 50;
```

只对长度 ≥ 50 的字符串做池化。原因很实在：

```
// 不池化（3 字节）
ɵɵelement(0, "div")

// 池化（更差）
const _c0 = "div";   // 声明本身就要十几个字节
ɵɵelement(0, _c0)    // 还要一次变量查找
```

**短字符串内联比引用更划算。** 池化只在常量足够大、或复用次数足够多时才划算。这个阈值就是那条权衡线。你在产物里看到的 `_cN` 通常都是数组、大字符串、或函数——正是"值得池化"的那些。

#### 2.9.4 实战：读懂产物里的 consts

组件的 `consts` 字段通常是这样：

```
consts: [[3, "title"], ["class", "foo"]]
```

这个数组是 `allocateSlots` 之后、`collectElementConsts` 阶段收集的元素静态属性。它们的索引和 `ɵɵelementStart` 的槽位号对应。**当你调试"某个元素的属性没生效"时，看 consts 数组能快速确认编译器到底把它识别成了什么属性。**

---

### 2.10 Partial Compilation 深入：`ɵɵngDeclareComponent` 的真面目

现在回到 §2.1.3 提出的第三条路。库发布时，组件被编译成什么样子？答案是：一个对 `ɵɵngDeclareComponent(...)` 的调用，参数是一个**纯数据对象**（元数据），不含任何 `ɵɵ` 指令。

#### 2.10.1 生成器：`compileDeclareComponentFromMetadata`

`packages/compiler/src/render3/partial/component.ts`：

```
export function compileDeclareComponentFromMetadata(
  meta: R3ComponentMetadata<R3TemplateDependencyMetadata>,
  template: ParsedTemplate,
  additionalTemplateInfo: DeclareComponentTemplateInfo,
): R3CompiledExpression {
  const definitionMap = createComponentDefinitionMap(meta, template, additionalTemplateInfo);
  const expression = o.importExpr(R3.declareComponent).callFn([definitionMap.toLiteralMap()]);
  const type = createComponentType(meta);
  return {expression, type, statements: []};
}
```

对比完整编译的 `compileComponentFromMetadata`（§2.6.4），差别一目了然：

|  | 完整编译 | 部分编译 |
| --- | --- | --- |
| 调用的函数 | `ɵɵdefineComponent` | `ɵɵngDeclareComponent` |
| 参数 | 含 `template` 函数（已生成指令） | 含 `template` **字符串** |
| 是否含 `ɵɵ` 指令 | 是 | **否** |
| 是否含 decls/vars | 是（编译期算好） | **否**（留给 linker 算） |

部分编译的产出长这样（概念示意）：

```
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({
  version: "22.2.3",
  minVersion: "17.0.0",
  type: MyComponent,
  selector: "my-card",
  inputs: {title: "title"},
  template: `<div class="card">{{title}}</div>`,   // ← 原始模板字符串！
  styles: [".card { padding: 8px }"],
  dependencies: [{kind: "component", type: Other, selector: "other-cmp"}],
  // ...
});
```

**模板是以字符串形式保留的。** 这就是"部分"的含义——编译只做了一半（解析了装饰器元数据、提取了模板字符串、识别了依赖），把真正的代码生成推迟到 linker。

#### 2.10.2 元数据里都有什么

`createComponentDefinitionMap` 逐个字段填充：

```
function createComponentDefinitionMap(meta, template, templateInfo) {
  const definitionMap = createDirectiveDefinitionMap(meta);   // 继承指令的公共字段
  const blockVisitor = new BlockPresenceVisitor();
  visitAll(blockVisitor, template.nodes);

  definitionMap.set('template', getTemplateExpression(template, templateInfo));

  if (templateInfo.isInline) {
    definitionMap.set('isInline', o.literal(true));
  }

  // 用了块语法（@if/@for/@defer 等）→ 要求 linker ≥ 17.0.0
  if (blockVisitor.hasBlocks) {
    definitionMap.set('minVersion', o.literal('17.0.0'));
  }

  definitionMap.set('styles', toOptionalLiteralArray(meta.styles, o.literal));
  definitionMap.set('dependencies', compileUsedDependenciesMetadata(meta));
  definitionMap.set('viewProviders', meta.viewProviders);
  definitionMap.set('animations', meta.animations);

  if (meta.changeDetection !== null) {
    definitionMap.set('changeDetection',
      o.importExpr(R3.ChangeDetectionStrategy).prop(core.ChangeDetectionStrategy[meta.changeDetection]));
  }
  if (meta.encapsulation !== core.ViewEncapsulation.Emulated) {
    definitionMap.set('encapsulation',
      o.importExpr(R3.ViewEncapsulation).prop(core.ViewEncapsulation[meta.encapsulation]));
  }
  if (template.preserveWhitespaces === true) {
    definitionMap.set('preserveWhitespaces', o.literal(true));
  }
  // ... @defer 依赖处理
  return definitionMap;
}
```

几个值得注意的点：

**①** `**BlockPresenceVisitor**`** 与** `**minVersion: '17.0.0'**`**。** 这是个精巧的**版本协商**机制。看这个 visitor：

```
class BlockPresenceVisitor extends RecursiveVisitor {
  hasBlocks = false;
  override visitDeferredBlock(): void { this.hasBlocks = true; }
  override visitDeferredBlockPlaceholder(): void { this.hasBlocks = true; }
  override visitDeferredBlockLoading(): void { this.hasBlocks = true; }
  override visitDeferredBlockError(): void { this.hasBlocks = true; }
  override visitIfBlock(): void { this.hasBlocks = true; }
  override visitIfBlockBranch(): void { this.hasBlocks = true; }
  override visitForLoopBlock(): void { this.hasBlocks = true; }
  override visitForLoopBlockEmpty(): void { this.hasBlocks = true; }
  override visitSwitchBlock(): void { this.hasBlocks = true; }
  override visitSwitchBlockCase(): void { this.hasBlocks = true; }
  override visitSwitchBlockCaseGroup(): void { this.hasBlocks = true; }
}
```

它遍历模板，只要发现了**任何一种块语法**，就设 `minVersion = '17.0.0'`。源码注释解释了这个设计的克制：

> We don't do this for templates without blocks, in order to **preserve backwards compatibility**. （对不含块的模板我们不设这个，以保持向后兼容。）

也就是说：**老库（用** `***ngIf**`** 写的）即使被新版 Angular 编译，也不会被强制要求 17.0.0+ 的 linker**，从而能被老版本的应用消费。只有用了新语法的库才"升级要求"。这是版本兼容上的精细权衡——**能力升级不该无谓地抬高门槛。**

**② 默认值字段被省略。** 注意 `if (meta.encapsulation !== core.ViewEncapsulation.Emulated)`——只有当封装模式**不是**默认的 `Emulated` 时才写进元数据。同理 `changeDetection` 为 `null`（即默认）时也不写。

这样做的好处是**元数据更小**，而且**未来改默认值不会破坏老库**。如果 Angular 23 把默认封装从 `Emulated` 改成别的，老库因为没写这个字段，会自动采用新默认值——而这通常正是期望的行为（库作者写"默认"时，意思是"跟随框架推荐"）。**"省略即默认"让库的语义能随框架演进而演进**，这是向前兼容的一个巧妙设计。

**③** `**dependencies**`** 的格式。** 依赖被写成带 `kind` 的结构：

```
case R3TemplateDependencyKind.Directive:
  dirMeta.set('kind', o.literal(decl.isComponent ? 'component' : 'directive'));
  dirMeta.set('type', wrapType(decl.type));
  dirMeta.set('selector', o.literal(decl.selector));
  dirMeta.set('inputs', toOptionalLiteralArray(decl.inputs, o.literal));
  dirMeta.set('outputs', toOptionalLiteralArray(decl.outputs, o.literal));
  dirMeta.set('exportAs', toOptionalLiteralArray(decl.exportAs, o.literal));
  return dirMeta.toLiteralMap();
case R3TemplateDependencyKind.Pipe:
  pipeMeta.set('kind', o.literal('pipe'));
  pipeMeta.set('type', wrapType(decl.type));
  pipeMeta.set('name', o.literal(decl.name));
  return pipeMeta.toLiteralMap();
```

linker 拿到这些 `kind` 就知道怎么重建依赖关系。注意 `wrapType`——根据 `declarationListEmitMode`，可能要包一层 `forwardRef`（处理循环引用）。

#### 2.10.3 模板字符串的三种处理

`getTemplateExpression` 处理三种情况，注释写得极详细：

```
function getTemplateExpression(template, templateInfo): o.Expression {
  // ① 内联且是字面量 → 直接用原表达式，保留 sourcemap
  if (templateInfo.inlineTemplateLiteralExpression !== null) {
    return templateInfo.inlineTemplateLiteralExpression;
  }

  // ② 内联但非字面量（通过静态求值得到）→ 建字面量，但无法提供 source span
  if (templateInfo.isInline) {
    return o.literal(templateInfo.content, null, null);
  }

  // ③ 外部文件 → 合成一个带 source span 的字面量，指向 .html 文件
  const contents = templateInfo.content;
  const file = new ParseSourceFile(contents, templateInfo.sourceUrl);
  const start = new ParseLocation(file, 0, 0, 0);
  const end = computeEndLocation(file, contents);
  const span = new ParseSourceSpan(start, end);
  return o.literal(contents, null, span);
}
```

**为什么这么讲究？** 因为 linker 之后还要**重新解析这个模板字符串**。如果模板报错（比如标签没闭合），错误信息要能指回**原始的** `**.html**`** 文件或** `**.ts**`** 文件的正确行列**。所以：

- 情况 ① 保留原始字面量表达式 → 直接映射到源文件的精确位置；
- 情况 ③ 外部文件 → 手工合成一个跨越整个文件的 source span（那个 `computeEndLocation` 就是在算"这份内容最后一行第几列"）。

**编译器的错误处理质量，取决于它保存了多少源码位置信息。** 这一段就是对这件事的执着。

### 2.11 Linker 深入：把半成品"链接"成成品

库发布的是 `ɵɵngDeclareComponent({...})` 这种半成品。应用构建时，需要有个东西把它变成 `ɵɵdefineComponent({...})` 成品。这个东西就是 **linker**。

#### 2.11.1 整体架构

```
应用构建流程
    │
    ├─ 打包器（webpack / esbuild / Vite）读源码
    │
    ├─ Babel 插件（@angular/compiler-cli/linker/babel）拦截 AST
    │      │
    │      ├─ 发现调用 ɵɵngDeclareComponent(...)
    │      │
    │      ├─ 提取参数（对象字面量）
    │      │
    │      ├─ FileLinker.linkPartialDeclaration(...)
    │      │      │
    │      │      ├─ PartialLinkerSelector 按版本选链接器
    │      │      │
    │      │      └─ PartialComponentLinkerVersion1.linkPartialDeclaration(...)
    │      │             │
    │      │             ├─ 重新解析 template 字符串
    │      │             ├─ 跑完整的 IR 流水线（ingest/transform/emit）
    │      │             └─ 返回 ɵɵdefineComponent({...}) 的 AST
    │      │
    │      └─ 用新 AST 替换原调用
    │
    └─ 输出不含 ɵɵngDeclare* 的最终代码
```

**关键点：linker 内部跑的是和 AOT 完全相同的编译器。** 它拿到模板字符串后，走的是 `parseTemplate` → IR 流水线 → `emitTemplateFn` 这一整套。区别只在于：AOT 是从 `.ts` 源文件里的装饰器拿元数据，linker 是从一个 JS 对象字面量里读元数据。

这就是为什么"部分编译"能保证产物和本机 AOT 一致——**它们共用同一个代码生成器**。

#### 2.11.2 `FileLinker`：单文件链接的主控

`packages/compiler-cli/linker/src/file_linker/file_linker.ts`：

```
export class FileLinker<TConstantScope, TStatement, TExpression, TType> {
  private linkerSelector: PartialLinkerSelector<TExpression>;
  private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression, TType>>();

  constructor(
    private linkerEnvironment: LinkerEnvironment<TStatement, TExpression, TType>,
    sourceUrl: AbsoluteFsPath,
    code: string,
  ) { /* ... */ }

  /** 这个函数名是不是一个可链接的部分声明？ */
  isPartialDeclaration(calleeName: string): boolean {
    return this.linkerSelector.supportsDeclaration(calleeName);
  }

  linkPartialDeclaration(
    declarationFn: string,
    args: TExpression[],
    declarationScope: DeclarationScope<TConstantScope, TExpression>,
  ): TExpression {
    // ① 校验：必须只有一个对象字面量参数
    if (args.length !== 1) {
      throw new Error(
        `Invalid function call: It should have only a single object literal argument, but contained ${args.length}.`,
      );
    }

    // ② 把参数解析成 AstObject（带类型的对象访问器）
    const metaObj = AstObject.parse<R3PartialDeclaration, TExpression>(
      args[0], this.linkerEnvironment.host,
    );

    // ③ 取出 ngImport（这个声明是从哪个模块导入的 ɵɵngDeclare*）
    const ngImport = metaObj.getNode('ngImport');
    const emitScope = this.getEmitScope(ngImport, declarationScope);

    // ④ 读版本号
    const minVersion = metaObj.getString('minVersion');
    const version = metaObj.getString('version');

    // ⑤ 按版本选链接器，执行链接
    const linker = this.linkerSelector.getLinker(declarationFn, minVersion, version);
    const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj, version);

    // ...
  }
}
```

几个设计要点：

**①** `**AstObject**`** 抽象。** linker 的难点在于它要操作**别人家的 AST**：Babel 的 AST 和 TypeScript 的 AST 完全不同。`AstObject` 提供了一层统一接口（`getString`、`getNode`、`getArray`...），让链接器代码不必关心底层是哪种 AST。这就是为什么 linker 能同时提供 Babel 插件（`linker/babel/`）和给其他工具集成的能力——**靠 AST 抽象层隔离了转译器差异**。

**②** `**emitScopes**`** 与** `**ngImport**`**。** 每个文件里可能有多个部分声明，它们可能各自 `import` 自不同的模块（通常是 `@angular/core`）。`ngImport` 节点标识了这个声明的"导入来源"，`emitScopes` 按导入来源分组管理——**同一个来源的声明共享一个常量池和输出作用域**，这样生成的常量（`_c0`）不会冲突、能跨声明复用。

**③ 版本从这里进入决策。** `minVersion` 和 `version` 被取出来，交给下一步的选择器。

#### 2.11.3 `PartialLinkerSelector`：用 semver 做版本协商

这是 linker 最精妙的部分。先看声明函数名清单：

```
export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareClassMetadata = 'ɵɵngDeclareClassMetadata';
export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent';
export const ɵɵngDeclareFactory = 'ɵɵngDeclareFactory';
export const ɵɵngDeclareInjectable = 'ɵɵngDeclareInjectable';
export const ɵɵngDeclareInjector = 'ɵɵngDeclareInjector';
export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule';
export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe';
export const ɵɵngDeclareClassMetadataAsync = 'ɵɵngDeclareClassMetadataAsync';
export const ɵɵngDeclareService = 'ɵɵngDeclareService';
```

十种声明函数，覆盖了组件、指令、管道、NgModule、可注入服务、工厂、注入器、类元数据。注意 `ɵɵngDeclareService` 是较新加的——standalone 时代的产物。

然后是按版本选择链接器的映射表：

```
export function createLinkerMap(...) {
  const linkers = new Map<string, LinkerRange<TExpression>[]>();
  const LATEST_VERSION_RANGE = getRange('<=', PLACEHOLDER_VERSION);

  linkers.set(ɵɵngDeclareComponent, [
    {range: LATEST_VERSION_RANGE, linker: new PartialComponentLinkerVersion1(...)},
  ]);
  // ... 其他同理
  return linkers;
}
```

关于这套机制的规则，源码注释给了完整说明：

> Versions should be sorted in ascending order. **The most recent partial-linker will be used as the fallback linker if none of the other version ranges match.**
>
> When there is a change to a declaration interface that requires a new partial-linker, the `minVersion` of the partial-declaration should be updated, the new linker implementation should be added to the end of the collection, and the version of the previous linker should be updated.

翻译一下这套"演进协议"：

1. 每个声明函数对应一组 `LinkerRange`（semver 范围 + 链接器实例）；
2. 范围按**升序**排列，第一个匹配的范围胜出；
3. 如果声明格式变了（需要新的链接器），就：① 提高新产物的 `minVersion`；② 把新链接器加到列表末尾；③ 更新上一个链接器的版本上限；
4. **最新的链接器作为兜底**——没有任何范围匹配时用它。

这就是 Angular 库能跨版本长期兼容的技术保障：**声明格式的每次变更都伴随一个版本号，linker 用 semver 判断"我认识这个格式吗"，并选择合适的解析器。**

#### 2.11.4 版本不匹配怎么办：`unknownDeclarationVersionHandling`

如果库的 `minVersion` 比当前 linker 支持的最高版本还新（比如库用 Angular 25 编译，应用用 Angular 22 的 linker），怎么办？

```
export interface LinkerOptions {
  sourceMapping: boolean;                      // 是否用 sourcemap 定位外部模板，默认 true
  linkerJitMode: boolean;                      // 是否为下游 JIT 编译器生成额外信息，默认 false
  unknownDeclarationVersionHandling: 'ignore' | 'warn' | 'error';
}

export const DEFAULT_LINKER_OPTIONS: LinkerOptions = {
  sourceMapping: true,
  linkerJitMode: false,
  unknownDeclarationVersionHandling: 'error',   // ★ 默认：直接报错
};
```

三种策略（源码注释解释得很清楚）：

- `**error**`**（默认）**：版本不匹配是致命错误，直接失败。**默认选最严格的**——与其让一个"可能链接错误"的库悄悄进入产物，不如早点失败让你知道"请升级 Angular"。这是正确的默认选择：**沉默的错误比响亮的失败危险得多。**
- `**warn**`：打警告，但还是用最新的链接器硬着头皮处理。适合"我知道版本略高但我确认格式没变"的场景。
- `**ignore**`：静默处理。最危险，只适合你完全掌控的环境。

另外两个选项：

- `**sourceMapping: true**`：linker 会用 sourcemap 把外部模板的错误位置映射回原始的 `.ts`/ `.html` 文件。这和第 2.10.3 讲的"保存源码位置"呼应——**整条链路都在为了"报错指得准"而努力**。
- `**linkerJitMode**`：JIT 模式下，NgModule 定义需要额外保留 `declarations`/ `imports`/ `exports` 等信息（AOT 下这些可以被消掉）。如果下游还要 JIT 编译，就得留着。

#### 2.11.5 Babel 插件：linker 的落地形态

`packages/compiler-cli/linker/babel/src/babel_plugin.ts`：

```
export function defaultLinkerPlugin(api: ConfigAPI, options: Partial<LinkerOptions>): PluginObject {
  api.assertVersion(8);   // 要求 Babel 8

  return createEs2015LinkerPlugin({
    ...options,
    fileSystem: new NodeJSFileSystem(),
    logger: new ConsoleLogger(LogLevel.info),
  });
}
```

注释说明了它的定位：

> This is the Babel plugin definition that is provided as a default export from the package, such that the plugin can be used using the module specifier of the package. **This is the recommended way of integrating the Angular Linker into a build pipeline other than the Angular CLI.**

也就是说：**Angular CLI 内部有自己集成 linker 的方式**，而如果你用的是别的构建工具（Vite、Rollup、webpack 等），官方推荐用这个 Babel 插件。这体现了 linker 的"可移植"设计——它不绑定任何构建工具，只依赖一个 AST 抽象。

`es2015_linker_plugin.ts` 里的实现负责：遍历 Babel AST，找到形如 `ɵɵngDeclareXxx({...})` 的调用，交给 `FileLinker` 处理，再用返回的 AST 替换原节点。

#### 2.11.6 为什么缓存到 `node_modules` 之外

回顾 README 那句话：

> These linked files can be cached outside `node_modules` so it does not suffer from problems of **mutating packages in** `**node_modules**`.

早期（View Engine 时代）的做法是：**直接改写** `**node_modules**`** 里的库文件**，把编译结果写回去。这个做法有一堆问题：

- 包管理器（npm/pnpm/yarn）会认为 `node_modules` 被篡改，校验失败；
- 缓存失效：每次 `npm ci` 都要重跑；
- 可移植性差：换台机器、 `node_modules` 重建后状态不一致；
- CI 里并行构建可能互相踩。

新版 linker 的做法是：**在打包过程中内存里转换**，需要持久化就缓存到 `node_modules` 之外的目录。`**node_modules**`** 保持只读、纯净**——这是现代构建工具的主流做法，也是 pnpm 那套严格依赖治理（第 1 章 §1.3）能成立的前提之一。

#### 2.11.7 三种编译形态的最终闭环

把 §2.1 到 §2.11 串起来，Angular 的编译体系是这样自洽的：

```
                    ┌──────────────────────────────────────┐
                    │   共享的编译器核心 packages/compiler   │
                    │   (ml_parser → R3 AST → IR → emit)    │
                    └──────────────────────────────────────┘
                         ▲            ▲             ▲
                         │            │             │
         ┌───────────────┘            │             └───────────────┐
         │                            │                             │
   ┌─────────┐                 ┌─────────────┐              ┌─────────────┐
   │   AOT   │                 │  Partial    │              │     JIT     │
   │ (ngtsc) │                 │ (库发布)     │              │  (运行时)    │
   └─────────┘                 └─────────────┘              └─────────────┘
   从装饰器读元数据               产出声明对象                  运行时解析
   直接生成 ɵcmp                 含模板字符串                   R3JitReflector
        │                            │                             │
        │                            ▼                             │
        │                     ┌─────────────┐                      │
        │                     │   Linker    │                      │
        │                     │ (应用构建)   │                      │
        │                     └─────────────┘                      │
        │                            │                             │
        └────────────┬───────────────┴──────────────┬──────────────┘
                     ▼                              ▼
              ɵɵdefineComponent                ɵɵdefineComponent
              （完整定义 + 指令）                 （运行时现场生成）
                     │                              │
                     └──────────► 运行时 render3 ◄──┘
```

**一个编译器核心，三种入口，同一个产物契约（**`**ɵɵdefineComponent**`**）。** 这就是 Angular 编译体系的完整图景。

### 2.12 ngtsc：AOT 的"总指挥"

前面讲的都是"编译器核心"（`packages/compiler`）。但真正把这套核心用在一次真实构建上的，是 `**ngtsc**`（`packages/compiler-cli/src/ngtsc/`）——Angular 的 TypeScript 编译器插件。理解它的工作方式，才能把编译链路和你的日常 `ng build` 对上。

#### 2.12.1 ngtsc 的本质：TypeScript 编译器 + Angular 分析层

ngtsc 不是"另一个编译器"，而是**寄生在 TypeScript 编译器之上的一层**。它利用 TS 的 `CustomTransformers` 机制：

```
ts.Program（TypeScript 的标准编译流程）
    │
    ├─ 分析阶段：ngtsc 遍历所有类，找出带 @Component/@Directive/... 的
    │     ├─ reflection：反射读取装饰器元数据
    │     ├─ partial_evaluator：静态求值装饰器参数（把表达式算成值）
    │     ├─ scope：确定每个组件的可见指令集（standalone imports / NgModule）
    │     └─ typecheck：生成 Type Check Block 做模板类型检查
    │
    ├─ 转换阶段：ngtsc 通过 CustomTransformer 改写 AST
    │     └─ 把 @Component 类转换成带 ɵfac / ɵcmp 静态属性的类
    │
    └─ 输出：TS 编译器照常输出 JS + .d.ts
```

这样做的好处是**复用 TS 的一切**：模块解析、类型系统、增量编译、声明文件生成、sourcemap。Angular 只需要做"Angular 特有"的那部分工作。

#### 2.12.2 ngtsc 的目录职责

`packages/compiler-cli/src/ngtsc/` 下的每个目录对应一个关注点，看名字就能猜出职责：

| 目录 | 职责 |
| --- | --- |
| `reflection/` | ★ 反射：从 TS AST 里读出装饰器、类成员、导入导出 |
| `partial_evaluator/` | ★ 静态求值：把 `@Component({template: A + B})` 这种表达式**算成具体值** |
| `annotations/` | 各类装饰器（@Component/@Directive/@Injectable/...）的分析器 |
| `scope/` | ★ 作用域：确定每个组件可见的指令/管道集合 |
| `typecheck/` | ★ 模板类型检查（生成 TCB） |
| `incremental/` | ★ 增量编译：只重新分析改动的部分 |
| `transform/` | TS AST 转换（把分析结果写回代码） |
| `diagnostics/` | 诊断信息（错误/警告的格式化） |
| `imports/` | 导入管理（自动添加缺失的 import） |
| `cycles/` | 循环引用检测 |
| `sourcemaps/` | 源码映射 |
| `hmr/` | 热模块替换支持 |
| `indexer/` | 索引（供 IDE 语言服务用） |
| `xi18n/` | i18n 消息提取 |
| `perf/` | 性能计时 |
| `program.ts` | 主控：协调整个编译流程 |
| `tsc_plugin.ts` | TS 插件入口 |

#### 2.12.3 `partial_evaluator`：一个被低估的关键组件

`@Component` 的参数不是字面量，而是**任意 TypeScript 表达式**：

```
const STYLES = ['.card {}'];

@Component({
  selector: 'my-' + NAME,             // 字符串拼接
  template: require('./tpl.html'),    // 函数调用（旧写法）
  styles: [...STYLES, extraStyle],    // 数组展开 + 变量引用
  providers: [{provide: API, useValue: CONFIG.url}],   // 对象嵌套
})
class MyCard {}
```

编译器需要知道这些参数的**具体值**才能工作。但 TS 编译器不会帮你"执行"这些表达式——它只给 AST。

`partial_evaluator/` 就是干这个的：**静态解释（static interpretation）** TypeScript 表达式的一个子集。它能：

- 求字面量的值（ `'a' + 'b'` → `'ab'`）；
- 跟随变量引用（找到 `STYLES` 的声明并取出它的值）；
- 展开数组/对象（ `[...a, b]`）；
- 解析导入（跨文件的变量引用）；
- 对函数调用等"无法静态求值"的情况，**优雅失败**并给出清晰的诊断（"这个装饰器参数必须是静态可求值的"）。

这也是为什么 Angular 对装饰器参数有"必须是静态可求值"的要求——**不是框架任性，是编译器必须能在不执行代码的情况下算出它的值**。你见过 `NG1010: Value at position X in the ... decorator is not statically analyzable` 这类错误，就来自这里。

#### 2.12.4 `incremental/`：为什么第二次构建那么快

`ng build --watch` 时，改一个文件后重新构建通常很快，靠的就是 `incremental/`。它的核心思想：

- **记住上一次的分析结果**（哪些类是什么组件、模板解析结果、作用域信息）；
- **只重新分析受影响的部分**：改了 `a.component.ts`，只有它自己和依赖它的组件需要重新分析；改了一个被很多组件 `import` 的指令，影响面就大得多；
- **结果按"依赖图"组织**，精确计算"脏"的范围。

Angular 还有更激进的一层：**编译结果按组件粒度缓存**。你改了 `A` 组件的模板，只要 `B` 组件不依赖 `A`，`B` 的编译产物直接从缓存复用。这让大型项目的增量构建能从分钟级降到秒级。

#### 2.12.5 `typecheck/`：模板错误的来源

§2.5.3 提到模板类型检查靠"生成 TS 代码让 TS 检查"。`typecheck/` 就是干这个的：

1. 为每个组件生成一个 **Type Check Block（TCB）**——一段虚拟的 TS 函数，把模板里的每个表达式"复现"一遍；
2. 把 TCB 塞进一个**虚拟的 TS 源文件**（shim，见 `shims/`）；
3. 让 TS 编译器像检查普通代码一样检查它；
4. 收集 TS 报的错，**映射回模板的行列**；
5. 以 Angular 的诊断格式呈现给你。

你在终端看到的这类报错：

```
src/app/card.component.html:3:12 - error TS2339: Property 'nmae' does not exist on type 'User'.
```

它的完整旅程是：模板第 3 行第 12 列 → 编译器生成 TCB → TS 报 TS2339 → Angular 映射回模板位置 → 展示。**"错误指得准"这件事，背后是** `**typecheck/**`** +** `**sourcemaps/**`** 两个模块在协作。**

#### 2.12.6 `hmr/` 与 `indexer/`：两个"顺带"的能力

- `**hmr/**`：热模块替换（Hot Module Replacement）。第 1 章 §1.2 提到 `r3_hmr_compiler.ts`，它生成的元数据让 dev server 能在**不刷新页面**的情况下替换组件实现。编译器在这里专门为 HMR 生成额外信息——**开发体验是靠编译器专门支持换来的**。
- `**indexer/**`：为 IDE 语言服务（ `language-service/` 和 VSCode 插件）生成索引，让"跳转到模板里引用的组件定义"这类功能成为可能。

#### 2.12.7 编译器的"可观测性"：perf 与 diagnostics

`perf/` 模块记录编译各阶段耗时，`diagnostics/` 统一格式化错误。这两者合起来让你能：

```
ng build --verbose    # 看到编译各阶段耗时
```

Angular 甚至有专门的编译性能剖析支持。**一个成熟的编译器，必须能回答"编译为什么慢、慢在哪"。**

---

### 2.13 实战：亲手观察编译链路

理论讲完，动手验证。这一节的所有操作都能在本仓库或你自己的 Angular 项目里执行。

#### 2.13.1 实验一：看真实应用的编译产物

最直接的方法——编译一个组件看输出。在本仓库里，编译器的单元测试有现成的"编译结果快照"。搜索 `packages/compiler-cli/test/` 下的编译测试，你会看到形如：

```
// 期望的编译产物
MyComponent.ɵcmp = i0.ɵɵdefineComponent({
  type: MyComponent,
  selectors: [["my-cmp"]],
  decls: 2,
  vars: 1,
  consts: [[3, "title"]],
  template: function MyComponent_Template(rf, ctx) {
    if (rf & 1) {
      i0.ɵɵelementStart(0, "div");
      i0.ɵɵtext(1);
      i0.ɵɵelementEnd();
    }
    if (rf & 2) {
      i0.ɵɵadvance();
      i0.ɵɵtextInterpolate1("Hello ", ctx.name, "!");
    }
  }
});
```

对照本章的知识，你能在这段产物里读出：

- `decls: 2` → `allocateSlots` 算出来：div 占槽 0，text 占槽 1；
- `vars: 1` → `countVariables` 算出来：一个插值绑定；
- `consts: [[3, "title"]]` → 常量池收集的元素静态属性（ `3` 可能是属性类型标记）；
- `if (rf & 1)` / `if (rf & 2)` → `maybeGenerateRfBlock` 生成的两个阶段；
- `ɵɵadvance()` → `generateAdvance` 插入的游标前进；
- `ɵɵtextInterpolate1` → `collapseSingletonInterpolations` 折叠出的专用指令。

**验证方法**：在你自己的 Angular 项目里跑 `ng build --configuration development`（保留可读的产物），然后去 `dist/` 里找组件的 JS 文件，就能看到真实产物。生产构建会被压缩，但结构一致。

#### 2.13.2 实验二：验证 `DomOnly` 优化

本章 §2.6.3 说 standalone 且无指令依赖的组件会用 `DomOnly` 模式编译。验证方法：

```
// A：纯展示组件，standalone，无 imports
@Component({
  selector: 'app-plain',
  template: `<div>{{msg}}</div>`,
})
class PlainComponent { msg = 'hi'; }

// B：用了指令的 standalone 组件
@Component({
  selector: 'app-with-dir',
  imports: [NgIf],
  template: `<div *ngIf="ok">{{msg}}</div>`,
})
class WithDirComponent { msg = 'hi'; ok = true; }
```

分别编译两者，对比产物大小和内容。你会观察到：

- A 的产物里**没有**指令匹配相关的代码路径，指令集更精简；
- B 的产物里有 `dependencies` 数组和指令实例化逻辑。

**实战启示**：把"纯展示"的叶子组件写成不依赖任何指令的 standalone 组件，不只是代码更清晰，**编译产物也更小更快**。这是 standalone + `DomOnly` 带来的隐性红利。

#### 2.13.3 实验三：观察常量池

写一个含重复字面量的模板：

```
<div class="card"><span class="card">a</span></div>
<div class="card"><span class="card">b</span></div>
```

编译后观察：那些 `'card'` 和属性数组是否被提升成了 `_cN`。再写一个超长的静态文本（>50 字符），看它是否被池化；再写一个短文本（<50 字符），看它是否被内联。**用这个实验亲手验证 §2.9.3 那个"50"的阈值。**

#### 2.13.4 实验四：查看 linker 的输入与输出

如果你有权限查看 `node_modules` 里的 Angular 官方库（如 `@angular/material`），打开它的 `.mjs` 文件，你会看到：

```
Button.ɵcmp = i0.ɵɵngDeclareComponent({
  version: "22.x.x",
  minVersion: "17.0.0",
  type: Button,
  selector: "button[mat-button]",
  template: `...原始模板字符串...`,
  // ...
});
```

这就是**部分编译的产物**——模板还在，没有 `ɵɵ` 指令。

然后在你应用的构建产物里搜同一个组件，你会看到它变成了：

```
Button.ɵcmp = i0.ɵɵdefineComponent({
  type: Button,
  decls: 3,
  vars: 2,
  template: function Button_Template(rf, ctx) { /* ɵɵ 指令 */ }
});
```

**同一次链条的两端，亲眼看到了 linker 做了什么。** 这个对比是理解 partial compilation 最直观的方式。

#### 2.13.5 实验五：故意触发编译错误，读诊断信息

编译器的诊断质量值得亲身体会。试几个：

```
// ① 模板里访问不存在的属性
@Component({template: `{{user.nmae}}`})
class C { user = {name: 'a'}; }
// → TS2339: Property 'nmae' does not exist on type '{name: string}'

// ② 绑定到不存在的指令输入
@Component({template: `<my-cmp [notAnInput]="1"></my-cmp>`, imports: [MyCmp]})
class C {}
// → NG8002: Can't bind to 'notAnInput' since it isn't a known property of 'my-cmp'

// ③ 装饰器参数不可静态求值
const x = Math.random();
@Component({template: TPL})   // TPL 来自运行时
class C {}
// → 装饰器参数必须静态可求值
```

每一条错误都带着**精确的行列位置**（来自 §2.5.3 的 sourcemap 映射）和**错误码**（第 1 章 §1.12.3 讲的 NGxxxx 体系）。拿到错误码去 `angular.dev/errors/NGxxxx` 能查到完整解释和修复建议。

#### 2.13.6 实验六：感受增量编译

在一个有几十个组件的项目里：

```
# 首次完整构建，记下耗时
time ng build

# 只改一个叶子组件的模板
echo '<!-- change -->' >> src/app/leaf.component.html

# 再次构建，对比耗时
time ng build
```

第二次应该明显更快（§2.12.4 的增量编译）。如果你改的是一个被大量组件 import 的核心指令，第二次会更慢——**因为影响面更大**。这个实验能让你直观理解"依赖图决定增量构建成本"这件事，对设计组件结构有直接指导意义：**让高频修改的组件尽量位于依赖图的叶子位置。**

#### 2.13.7 实战小结：你现在能做的三件事

1. **读懂产物**：看到任何一段 `ɵcmp` 产物，能反推出它对应的模板结构和编译阶段；
2. **定位问题**：编译报错时，知道错误来自链路的哪一层（解析 / 绑定 / 类型检查 / 元数据求值），能更快地定位；
3. **优化结构**：知道 standalone + 无指令依赖 → `DomOnly`；高频改动放叶子 → 增量构建快。

### 2.14 编译链路全景：一张表看完整个过程

把本章所有内容压缩成一张"从源码到产物"的全景表。建议收藏，读后续章节时反复对照。

| 阶段 | 输入 | 输出 | 核心模块 | 关键函数/概念 |
| --- | --- | --- | --- | --- |
| ① 反射 | TypeScript 源文件 | 装饰器元数据 + 类信息 | `ngtsc/reflection` | 读 `@Component({...})` |
| ② 静态求值 | 装饰器参数 AST | 具体值（字符串、数组、对象） | `ngtsc/partial_evaluator` | 不求值则报 NG1010 |
| ③ 模板解析 | 模板字符串 | HTML AST | `ml_parser` | lexer → parser |
| ④ 表达式解析 | `{{...}}` 里的文本 | 表达式 AST | `expression_parser` | 受限 JS 子集 |
| ⑤ R3 转换 | HTML AST | R3 AST | `r3_template_transform` | `Text`/`BoundText`/`IfBlock`... |
| ⑥ 绑定解析 | R3 AST 的属性 | 属性/事件/指令输入判定 | `binding_parser` | `[x]` / `(x)` / `[(x)]` |
| ⑦ 指令匹配 | R3 AST + 可见指令集 | 每元素的匹配指令 | `view/t2_binder` | CSS 选择器匹配 |
| ⑧ 类型检查 | R3 AST | 诊断信息（错误/警告） | `ngtsc/typecheck` | Type Check Block |
| ⑨ ingest | R3 AST | IR（op 列表） | `pipeline/src/ingest` | `ingestComponent` |
| ⑩ transform | IR | 优化后的 IR | `pipeline/src/phases` | 74 个阶段（Tmpl 跑 57 个） |
| ⑪ 槽位分配 | IR | 带槽位号的 IR | `phases/slot_allocation` | ★ 产出 `decls` |
| ⑫ 变量计数 | IR | 带变量数的 IR | `phases/var_counting` | ★ 产出 `vars` |
| ⑬ 生成 advance | IR | 含 `ɵɵadvance` 的 IR | `phases/generate_advance` | 游标前进 |
| ⑭ 命名 | IR | 各视图有名 | `phases/naming` | `Cmp_Template` |
| ⑮ 实体化 | IR | 指令调用表达式 | `phases/reify` | 抽象 op → `ɵɵproperty(...)` |
| ⑯ 链式优化 | 指令调用 | 链式调用 | `phases/chain` | `ɵɵproperty(a)(b)(c)` |
| ⑰ emit | IR | 输出 AST | `pipeline/src/emit` | `emitView` + `maybeGenerateRfBlock` |
| ⑱ 常量池 | 输出 AST | 顶层 `_cN` 常量 | `constant_pool` | `FixupExpression` 回填 |
| ⑲ 写回 TS | 输出 AST | 带 `ɵfac`/`ɵcmp` 的类 | `ngtsc/transform` | TS CustomTransformer |
| ⑳ 输出 | TS | JS + `.d.ts` + sourcemap | TypeScript 编译器 | —— |

**如果是库**：①②③ 之后不走 ⑨–⑳，而是走 **partial 编译**——产出 `ɵɵngDeclareComponent({template: '<原始字符串>'})`。等应用构建时，linker 从声明对象里读出元数据，从 ③ 重新开始跑完整链路。

#### 2.14.1 两条路径的分岔点

```
                 装饰器元数据 + 模板
                        │
          ┌─────────────┴─────────────┐
          │                           │
     应用（AOT）                   库（Partial）
          │                           │
   继续走 ③→⑳                   只走到 ①②③
   产出 ɵɵdefineComponent        产出 ɵɵngDeclareComponent
          │                       （保留模板字符串）
          │                           │
          │                      应用构建时
          │                           │
          │                      Linker 读回元数据
          │                      从 ③ 继续走到 ⑳
          │                           │
          └──────────┬────────────────┘
                     ▼
             ɵɵdefineComponent
          （完全相同的产物形态）
```

分岔点在"是否立即做代码生成"，但最终汇合到同一个产物契约。**这就是 partial compilation 设计得漂亮的地方：两条路殊途同归，中间的差异被 linker 完全吸收。**

---

### 2.19 全章贯通：一个模板字符串到两条函数的完整旅程

本章讲了编译器的九个阶段，这一节把它们收成一条单向的数据流，看一个模板字符串到底经历了什么。全程只讲"输入是什么、输出是什么、为什么必须有这一步"。

**① 解析：字符串 → HTML AST（2.3）。** 输入是模板文本，输出是一棵 HTML 节点树。这一步**不理解 Angular 语义**，`*ngIf` 和 `@if` 在它眼里只是属性或文本块。分开的意义是：HTML 解析可以被独立测试、独立复用（语言服务也用它）。

**② 升维：HTML AST → R3 AST（2.4）。** 这一步注入 Angular 语义：`{{ }}` 变成 `Interpolation` 节点，绑定变成 `BoundAttribute`，事件变成 `BoundEvent`，控制流块被识别成 `IfBlock` / `SwitchBlock` / `DeferredBlock`。**这是 Angular 对模板的第一层理解**，也是模板类型检查的输入。

**③ 匹配与类型检查（2.5）。** 编译器拿着 R3 AST 去问"这个元素上有哪些指令匹配"，然后把绑定和指令输入对上，并检查类型。这一步是 `strictTemplates` 的实现位置，也是**编译期能做的最有价值的一类检查**——写错一个输入名在构建时就报错，而不是运行时静默失效。

**④ 降级为 IR（2.6）。** 从 Angular 17 起，编译器中间多了一层显式 IR。R3 AST 被降级成一串 `op`，每个 op 带若干 `trait`。这一层存在的理由是**让后续变换可组合、可复用、可单独测试**：57 趟变换（2.7）每一趟只做一件事，按顺序跑一遍。这跟现代编译器的 pass 流水线是同一个思路。

**⑤ 发射（2.8、2.9）。** 变换跑完后发射成 JS。这里的关键产出就是 1.10 见过的两条函数：creation 与 update。发射时还有个容易被忽略的机制——**常量池**：模板里所有字面量被抽成 `_c0`、`_c1`（2.9），这样重复的样式数组、净化器配置在整个应用里只存在一份，且能被压缩器更好地处理。

**⑥ 当你发布的是库：partial compilation（2.10、2.11）。** 应用可以 AOT 全编译，但库不行——库发布时不知道使用者的编译选项。所以库的编译产物是 `ɵɵngDeclareComponent(...)` 这种"半成品"，把元数据原样保留；使用者的构建里由 **linker** 在 Babel 层面把它翻译成真正的 `ɵcmp`。这条设计的代价是 linker 必须理解所有元数据形状，收益是**同一个 npm 包能被不同 Angular 版本消费**。

**⑦ 总指挥：ngtsc（2.12）。** 它不是一个独立编译器，而是 TypeScript 的一个 plugin：挂在 TS 程序上，借用 TS 的类型系统与增量能力，同时驱动上面所有阶段。理解这一点就能理解为什么 Angular 的编译错误经常表现为 TS 错误——**它们共用同一条诊断通道**。

这条流读下来最值得记住的是：**Angular 的模板不是"运行时解释"的，而是被翻译成了一串普通函数调用**。你在 devtools 里看到的 `ɵɵelementStart` 不是框架在运行，是编译产物在原样执行。

### 2.15 常见坑清单

**Q1：AOT 和 JIT 产出的** `**ɵcmp**`** 一样吗？** 结构一致（都是 `ɵɵdefineComponent` 的产物），但**生成时机不同**：AOT 在构建时由 ngtsc 生成并写进 JS；JIT 在浏览器运行时由 `@angular/compiler` 现场生成。另外 JIT 下模板里的错误**只能到运行时才发现**，而 AOT 在构建时就报出来。

**Q2：为什么修改模板后有时构建很慢？** 看 §2.12.4。增量编译按依赖图传播"脏"：改一个被大量组件 `import` 的核心指令，会让所有依赖它的组件重新编译。**优化方向**：把高频修改的部分放在依赖图叶子侧；减少不必要的 `imports` 扩散。

**Q3：**`**minVersion: '17.0.0'**`** 是什么意思？能不能手动改？** 它是 linker 的版本协商信号，表示"处理这份声明需要 ≥17.0.0 的 linker"。它由编译器**根据模板是否使用了块语法**自动设置（§2.10.2 的 `BlockPresenceVisitor`）。**不要手动改**——改低了会让老 linker 尝试解析它不认识的语法，导致链接错误。

**Q4：为什么我的库发布后，用户升级 Angular 不需要重新发布库？** 这正是 partial compilation 的价值（§2.1.3）。库产物里没有 `ɵɵ` 指令，只有声明式元数据 + 模板字符串。用户用新版本 Angular 构建时，新版 linker 会重新解析这些元数据、用新版编译器生成代码。**库与运行时实现解耦了。**

那什么情况下库**必须**重新发布？当声明格式本身发生不兼容变更时——此时新库会带更高的 `minVersion`，老 linker 会因为 `unknownDeclarationVersionHandling: 'error'`（默认）直接报错提示升级。

**Q5：**`**ɵɵ**`** 指令会不会在版本间变化？** 会。它们是内部实现细节（第 1 章 §1.10.1 明确说"not meant for developer consumption"）。**你的应用代码不该直接依赖它们**；但这也正是库采用 partial 格式的原因——库里没有 `ɵɵ`，所以不受影响。而**应用**每次构建都会用当前的编译器重新生成，自然与当前运行时匹配。

**Q6：模板类型检查为什么有时"太严格"或"太宽松"？** 它用的是 TypeScript 的类型系统（§2.5.3），所以严格程度取决于你的 `tsconfig` 里 `strictTemplates`（Angular 特有）和相关 TS 严格开关。开启 `strictTemplates` 后，模板里的类型检查会接近 TS 的严格程度——这是推荐做法，能在编译期抓住大量 bug。

**Q7：**`**decls**`**/**`**vars**`** 我能手动指定吗？** 不能，也不需要。它们是 `allocateSlots` 和 `countVariables` 根据模板结构自动算出来的（§2.7.5）。手动写只会写错——写小了数组越界，写大了浪费内存。

**Q8：为什么有的组件产物里有多个函数（**`**Cmp_Template**`**、**`**Cmp_Conditional_0_Template**`**）？** 每个**嵌入式视图**（`@if` 块内、`@for` 块内、`<ng-template>` 内）都是一个独立的视图，有自己独立的模板函数（§2.8.1）。它们被作为顶层语句放进常量池，由 `ɵɵtemplate` 指令在运行时引用。

**Q9：编译器报错"装饰器参数不可静态求值"怎么办？** 见 §2.12.3。`partial_evaluator` 必须能在不执行代码的情况下算出装饰器参数的值。常见触发：用了函数调用、用了运行时变量、跨了无法静态分析的模块边界。**解法**：把动态部分移到组件内部（用字段、用注入的服务），让装饰器参数保持静态。

**Q10：IR 那 74 个阶段，我需要都读懂吗？** 不需要。建议的策略是：

1. 先理解 §2.7 的**分组脉络**（语义规范化 → 控制流 → 名称解析 → i18n → 槽位/变量 → 命名 → 收尾优化 → 实体化 → 链式）；
2. 重点读与你想研究的问题相关的那几个（比如想研究 `@for`，读 `track_fn_optimization`；想研究水合，读 `insert_incremental_hydration_runtime`）；
3. 其余作为"知道存在"的清单，需要时再查。

### 2.16 本章核心结论

这一章我们从"模板字符串"走到了"ɵɵ 指令"，把 Angular 编译器的整条链路拆了个透。回顾核心认知：

**三种编译形态（§2.1）**：

- **JIT**：浏览器运行时编译，靠 `R3JitReflector` 查表解析符号，用于调试和动态场景；
- **AOT**：构建时编译（ngtsc），产物不含编译器，是生产默认；
- **Partial + Linker**：库发布半成品声明（保留模板字符串、不含 `ɵɵ`），应用构建时由 linker 链接成完整定义，**彻底解决了库与应用版本锁死**。

**编译流水线（§2.2–§2.8）**：

- 三种 AST 分工：HTML AST（结构）→ R3 AST（Angular 语义）→ IR（可优化）→ 输出 AST（代码）；
- **IR + 多趟变换**是 Angular 17+ 的核心架构：74 个阶段（Tmpl 跑 57 个），每个阶段独立、可测试；
- 阶段顺序有内在逻辑：**语义规范化 → 结构 → 名称解析 → 槽位/变量分配 → 命名 → 优化 → 实体化 → 链式**；
- `**decls**`** 来自** `**allocateSlots**`**（按 create 阶段 op 顺序分配槽位），**`**vars**`** 来自** `**countVariables**`**（先顶层后表达式内，两趟计数）**——这两个数字终于有了算法级解释；
- **两阶段函数由** `**maybeGenerateRfBlock(1/2)**`** 生成**，空的那一段根本不产出（ `if` 块都不生成）。

**工程细节中的智慧**：

- `ConstantPool` 用 `FixupExpression` 做"先占位后回填"，并给字符串设了 50 字符的池化阈值——**优化也要算账**；
- `minVersion: '17.0.0'` 只在模板用到块语法时才设置——**能力升级不该无谓抬高门槛**；
- 默认值字段被省略（ `encapsulation` 非默认才写）——**"省略即默认"让库能随框架演进**；
- linker 用 semver 做版本协商，默认 `error` 策略——**沉默的错误比响亮的失败危险**；
- 链接结果缓存到 `node_modules` 之外——**保持 node\_modules 只读纯净**。

**ngtsc（§2.12）**：寄生在 TypeScript 之上，靠 `reflection` 读元数据、`partial_evaluator` 静态求值、`scope` 算可见指令、`typecheck` 生成 TCB 做类型检查、`incremental` 支撑增量构建。

带着这些认知，下一章我们回到运行时，进入本系列的重头戏——**Signals 的图算法**。你会发现一个有趣的呼应：编译器把"决策提前到编译期"，而 signals 把"刷新精确化到运行期"——**两者都在追求同一件事：只做必要的工作。**

---

### 附录 A：本章关键源码文件速查表

| 你想知道 | 去看这个文件 |
| --- | --- |
| 三种编译形态的选择 | `packages/compiler/src/render3/r3_jit.ts`（JIT 反射器） |
| HTML 词法/语法分析 | `packages/compiler/src/ml_parser/`（lexer.ts / parser.ts / ast.ts） |
| 表达式解析 | `packages/compiler/src/expression_parser/` |
| R3 AST 节点定义 | `packages/compiler/src/render3/r3_ast.ts` |
| HTML AST → R3 AST | `packages/compiler/src/render3/r3_template_transform.ts` |
| 绑定判定 | `packages/compiler/src/template_parser/binding_parser.ts` |
| 指令/管道匹配 | `packages/compiler/src/render3/view/t2_binder.ts` |
| 模板解析入口 | `packages/compiler/src/render3/view/template.ts`（`parseTemplate`） |
| **编译组件主流程** | `packages/compiler/src/render3/view/compiler.ts`（`compileComponentFromMetadata`） |
| **IR 流水线主控** | `packages/compiler/src/template/pipeline/src/emit.ts`（`transform` / `emitTemplateFn` / `emitView`） |
| **74 个变换阶段** | `packages/compiler/src/template/pipeline/src/phases/` |
| `decls` 的算法 | `.../phases/slot_allocation.ts`（`allocateSlots`） |
| `vars` 的算法 | `.../phases/var_counting.ts`（`countVariables`） |
| `ɵɵadvance` 的生成 | `.../phases/generate_advance.ts` |
| IR 节点与 trait | `packages/compiler/src/template/pipeline/ir/` |
| 编译模式（Full/DomOnly） | `.../pipeline/src/compilation.ts` |
| 常量池 | `packages/compiler/src/constant_pool.ts` |
| 部分编译（声明生成） | `packages/compiler/src/render3/partial/`（component.ts / api.ts） |
| Linker 主控 | `packages/compiler-cli/linker/src/file_linker/file_linker.ts` |
| Linker 版本协商 | `.../file_linker/partial_linkers/partial_linker_selector.ts` |
| Linker 选项 | `.../file_linker/linker_options.ts` |
| Babel 插件 | `packages/compiler-cli/linker/babel/src/babel_plugin.ts` |
| ngtsc 主控 | `packages/compiler-cli/src/ngtsc/program.ts` |
| 静态求值 | `packages/compiler-cli/src/ngtsc/partial_evaluator/` |
| 模板类型检查 | `packages/compiler-cli/src/ngtsc/typecheck/` |

### 附录 B：74 个变换阶段速查（按功能分组）

| 分组 | 阶段 |
| --- | --- |
| 语义规范化 | `resolveForeignContent` `removeContentSelectors` `optimizeRegularExpressions` `emitNamespaceChanges` `deduplicateTextBindings` `specializeStyleBindings` `specializeBindings` `specializeControlProperties` `convertAnimations` `extractAttributes` `parseExtractedStyles` `removeEmptyBindings` `collapseSingletonInterpolations` |
| 控制流与结构 | `orderOps` `generateConditionalExpressions` `generateBoundaryConditions` `createPipes` `configureDeferInstructions` `insertIncrementalHydrationRuntime` `createVariadicPipes` `generateArrowFunctions` `generatePureLiteralStructures` `generateProjectionDefs` `generateLocalLetReferences` `generateVariables` `resolveBoundaries` `saveAndRestoreView` |
| 名称与上下文 | `deleteAnyCasts` `removeSafeNavigationMigration` `resolveDollarEvent` `generateTrackVariables` `removeIllegalLetReferences` `resolveNames` `resolveDeferTargetNames` `transformTwoWayBindingSet` `optimizeTrackFns` `resolveContexts` `resolveSanitizers` `liftLocalRefs` `expandSafeReads` `stripNonrequiredParentheses` `generateTemporaryVariables` `optimizeVariables` `optimizeStoreLet` |
| i18n | `propagateI18nBlocks` `wrapI18nIcus` `createI18nContexts` `convertI18nText` `convertI18nBindings` `removeUnusedI18nAttributesOps` `assignI18nSlotDependencies` `applyI18nExpressions` `resolveI18nElementPlaceholders` `resolveI18nExpressionPlaceholders` `extractI18nMessages` `collectI18nConsts` `resolveI18nAttrSanitizers` |
| **槽位与变量** | `**allocateSlots**`**（decls）** `**countVariables**`**（vars）** `**generateAdvance**` |
| 收尾与发射 | `nameFunctionsAndVariables` `resolveDeferDepsFns` `mergeNextContextExpressions` `generateNgContainerOps` `collapseEmptyInstructions` `attachSourceLocations` `disableBindings` `extractPureFunctions` `reify` `chain` `emptyElements` `nonbindable` `ngContainer` `hostStylePropertyParsing` `nextContextMerging` `namespace` `ordering` `temporaryVariables` `variableOptimization` `pipe_creation` `pipe_variadic` `pure_function_extraction` `pure_literal_structures` `local_refs` `const_collection` `chaining` `naming` `reify` `resolveBoundaries` |

### 附录 C：编译产物字段对照表

看到一段 `ɵɵdefineComponent({...})` 时，按这张表反推：

| 字段 | 来源 | 含义 |
| --- | --- | --- |
| `type` | 组件类 | 组件构造函数 |
| `selectors` | `@Component.selector` | 选择器 |
| `decls` | `**allocateSlots**` | 模板里的声明节点数（DECLS 段大小） |
| `vars` | `**countVariables**` | 模板里的绑定数（VARS 段大小） |
| `consts` | `**ConstantPool**`** +** `**collectElementConsts**` | 元素静态属性/常量数组 |
| `template` | `**emitTemplateFn**` | 两阶段模板函数 |
| `dependencies` | `**t2_binder**`** 匹配结果** | 模板用到的指令/管道 |
| `ngContentSelectors` | IR 的 `contentSelectors` | `<ng-content>` 的选择器 |
| `encapsulation` | 非默认时才写 | 样式封装模式 |
| `changeDetection` | 非默认时才写 | 变更检测策略 |
| `features` | `addFeatures` | 各类特性（Providers、ExternalStyles、Control 等） |
| `hostBindings` | **Host 编译任务** | 宿主绑定函数 |
| `inputs` / `outputs` | 装饰器元数据 | 输入输出映射 |

---

### 思考题

1. AOT、JIT、Partial 三种编译形态各自的适用场景是什么？Partial 解决了什么历史难题？
2. 为什么库不能发布"完全编译"的产物？如果发布了会发生什么？
3. 编译过程中 AST 换了几次形态？每次转换解决什么问题？
4. `decls: 2` 这个数字是怎么算出来的？请描述 `allocateSlots` 的算法。
5. `countVariables` 为什么要分两趟（先顶层、后表达式内）？不分会有什么问题？
6. `TemplateCompilationMode.DomOnly` 是什么？什么条件下会启用？它带来什么好处？
7. `ɵɵngDeclareComponent` 里的 `minVersion` 为什么只在用了块语法时才设为 `17.0.0`？
8. linker 遇到"不认识的声明版本"时默认怎么处理？为什么默认策略是这样？
9. `ConstantPool` 为什么需要 `FixupExpression`？那个"50 字符阈值"背后的权衡是什么？
10. 模板里 `{{user.nmae}}` 报错，这个错误是怎么产生的？经过了哪几个模块？
11. 如果你要往编译器里加一个新的优化，应该插在阶段列表的哪个位置？判断依据是什么？
12. 为什么 standalone + 无指令依赖的组件编译产物更优？这对你的组件设计有什么启示？

---

### 2.17 深入：IR 的数据结构 —— op、trait 与 handle

§2.7 讲了"74 个阶段在做什么"，但没说清**它们操作的数据长什么样**。这一节补上这个缺口。理解 IR 的数据模型，你才能真正读懂那些阶段的代码。

#### 2.17.1 一切皆 op：用链表组织的双向序列

IR 的基本单位是 **op（operation）**，定义在 `template/pipeline/ir/src/ops/` 下，按所属阶段分四个文件：

```
ops/
├── create.ts   # create 阶段的 op（建结构）
├── update.ts   # update 阶段的 op（刷绑定）
├── host.ts     # 宿主绑定的 op
└── shared.ts   # 两阶段共用的 op
```

`OpKind` 枚举（`ir/src/enums.ts`）列出了全部 op 类型，挑几个认识一下：

```
export enum OpKind {
  /** 特殊 op：表示 op 链表（双向）的头尾哨兵节点 */
  ListEnd,

  /** 包装了一个输出 AST 语句的 op——即"已经落地成代码"的 op */
  Statement,

  /** 声明并初始化一个 SemanticVariable */
  Variable,

  /** 开始渲染一个元素 */
  ElementStart,
  /** 渲染一个无子元素的元素 */
  Element,
  /** 渲染一个外部（foreign）组件 */
  ForeignComponent,
  /** 声明一个嵌入式视图 */
  Template,
  /** 结束 ElementStart 开始的元素 */
  ElementEnd,
  /** 开始一个 ng-container */
  ContainerStart,
  // ... 还有几十种
}
```

两个细节值得注意：

**①** `**ListEnd**`** 是哨兵节点。** IR 的 op 不是存在数组里，而是组织成**带头尾哨兵的双向链表**。为什么不用数组？因为编译器要频繁地**插入、删除、重排** op（想想 `orderOps`、`chain`、`mergeNextContextExpressions` 这些阶段）。链表上插入删除是 O(1)，数组则要搬移元素。这个选择在"编译器要反复改写 IR"的场景下是对的。

**②** `**Statement**`** 是一个特殊 op 类型。** 回忆 §2.8.2 里 `emitView` 断言 `op.kind !== ir.OpKind.Statement` 就抛错——那时每个 op 都**必须**已经变成 `Statement`。也就是说，`Statement` 是 op 的"终态"：抽象的 `ElementStart` op 经过 `reify` 之后，就变成了一个包着 `ɵɵelementStart(...)` 调用的 `Statement` op。**IR 的演化过程，本质上是 op 从"抽象形态"变成"Statement 形态"的过程。**

#### 2.17.2 Trait：给 op 打的能力标签

不同 op 有不同能力：有的占槽位、有的消耗变量、有的产生变量。IR 用 **trait（特质）** 来标记这些能力，定义在 `ir/src/traits.ts`：

```
/** 表示这个 op 会消耗数据槽位 */
export interface ConsumesSlotOpTrait {
  readonly [ConsumesSlot]: true;
  /**
   * Assigned data slot (the starting index, if more than one slot is needed) for this operation, or
   * `null` if slots have not yet been assigned.
   */
  handle: SlotHandle;
  // ...
}

/** 表示这个 op 会消耗变量槽位 */
export interface ConsumesVarsTrait {
  [ConsumesVarsTrait]: true;
}

/** 表示一个表达式需要知道"它之前用了多少个变量槽" */
export interface UsesVarOffsetTrait {
  // ...
}
```

回忆 §2.7.5 的两个阶段：

```
// allocateSlots
if (!ir.hasConsumesSlotTrait(op)) continue;   // 只处理带 ConsumesSlotOpTrait 的
op.handle.slot = slotCount;
slotCount += op.numSlotsUsed;

// countVariables
if (ir.hasConsumesVarsTrait(op)) {            // 只处理带 ConsumesVarsTrait 的
  varCount += varsUsedByOp(op);
}
```

**这就是 trait 的价值**：阶段不需要知道"具体有哪些 op 类型"，只需要问"你有这个 trait 吗"。新增一种 op 时，只要给它打上正确的 trait，所有相关阶段就自动处理它了。这是典型的\*\*开闭原则（对扩展开放、对修改封闭）\*\*应用——在 74 个阶段的系统里，这个设计至关重要，否则加一个 op 要改十几个阶段。

#### 2.17.3 Handle：延迟绑定的引用

```
// ir/src/handle.ts
export class SlotHandle {
  slot: number | null = null;
}
```

`SlotHandle` 是个极简的类，只装一个可能为 `null` 的槽位号。它的存在形式很有意思：**op 不直接持有** `**slot: number**`**，而是持有一个** `**handle**`** 对象**。

为什么？因为**槽位号在 ingest 阶段还不知道**（要等 `allocateSlots` 跑完）。如果 op 直接持有一个数字，那在分配之前就得填个占位值（比如 -1），而且所有"引用这个 op 的槽位"的表达式都要在分配后**逐个回填**。

用 handle 的话：

```
// ingest 时：op 创建 handle，表达式持有同一个 handle 对象
op.handle = new SlotHandle();          // slot: null
someExpr.slot = op.handle;             // 表达式引用同一个对象

// allocateSlots 时：只改一处
op.handle.slot = 42;                   // 所有引用者自动"看到" 42
```

**用一层间接引用（handle），把"N 处回填"变成"一处赋值"。** 这和 §2.9.2 的 `FixupExpression` 是同一个思路——**用间接层解决"决策晚于使用"的时序问题**。编译器里这类手法反复出现，认出它能大幅降低读码难度。

`handle.slot === null` 时的语义是"槽位还没分配"，这也是为什么 trait 的注释里写 `or null if slots have not yet been assigned`——`null` 是有意义的合法状态，不是错误。

#### 2.17.4 三种 IR 表达式与变量

`ir/src/expression.ts` 和 `variable.ts` 定义了 IR 层的表达式扩展。IR 的表达式**不是**输出 AST 的表达式，而是"带 Angular 语义的表达式"：

- `SemanticVariable`：语义变量（对应模板里的局部变量、 `@for` 的 item 变量等）；
- 各类 IR 特有的读取操作（读取上下文、读取局部变量、纯函数调用……）。

这些 IR 表达式最终会在 `reify` 阶段被转换成输出 AST 的表达式（如 `ɵɵreadContextLet(...)`）。**IR 表达式层是"语义"和"代码"之间的最后一道缓冲**——它让 74 个阶段可以在"还不知道最终代码长什么样"的情况下自由地分析和变换。

#### 2.17.5 数据模型小结

把这一节串起来，IR 的完整数据模型是：

```
CompilationJob                     ← 一次编译任务（Tmpl 或 Host）
├── units: ViewCompilationUnit[]   ← 多个编译单元（根视图 + 每个嵌入式视图）
│   ├── create: op 链表（双向，带 ListEnd 哨兵）  ← create 阶段的操作
│   ├── update: op 链表                          ← update 阶段的操作
│   ├── decls / vars                             ← 由 allocateSlots / countVariables 填
│   └── xref / parent                            ← 视图间关系
└── pool: ConstantPool             ← 共享常量池

op                                 ← 基本单位
├── kind: OpKind                   ← 类型（ElementStart / Property / Statement ...）
├── traits                          ← 能力标签（ConsumesSlot / ConsumesVars ...）
├── handle: SlotHandle             ← 延迟绑定的槽位引用
└── statement?                      ← reify 后填入的真实代码
```

**链表组织 + trait 标记 + handle 间接**——这三者的组合，让 74 个阶段能各自独立地增删改查 IR，而不互相纠缠。理解了这套数据模型，你再打开任何一个 `phases/*.ts` 文件，都能立刻看懂它在操作什么。

---

> **下一章预告**：第 03 章《Signal 图算法：producer/consumer 依赖图、版本号与 glitch-free》。我们将钻进 `packages/core/primitives/signals/src/graph.ts`，看 Angular 22 的整个反应式系统是如何建立在一张"带版本号的依赖图"之上的——包括为什么它能做到"无中间态（glitch-free）"、为什么不需要手动声明依赖、以及内存如何回收。这是全系列技术含量最高的一章之一。

---

### 附录 D：编译期决策 vs 运行时决策对照表

第 1 章 §1.10.4 讲过 Ivy 的核心设计原则——"**把 if 从运行时挪到编译期**"。本章走完编译链路后，可以系统性地列出这条原则的落实清单。这张表是理解 Angular "为什么快"的总纲：

| 决策 | 谁决定 | 运行时是否还要判断 |
| --- | --- | --- |
| 模板结构（有哪些节点） | **编译期**（ingest + 各阶段） | 否，按槽位直接访问 |
| `decls` / `vars`（数组大小） | **编译期**（allocateSlots / countVariables） | 否，创建时按数分配 |
| 每个节点的槽位号 | **编译期** | 否，硬编码在指令调用里 |
| 绑定是 DOM 属性还是指令输入 | **编译期**（binding\_parser + t2\_binder） | 否，直接生成对应指令 |
| 绑定是否需要 XSS 消毒 | **编译期**（resolveSanitizers） | 否，需要的地方才插入消毒 |
| 插值是几个（用哪个 `*InterpolateN`） | **编译期**（collapseSingletonInterpolations） | 否，专用指令直接生成 |
| 是否需要 `ɵɵadvance` | **编译期**（generateAdvance） | 否，游标位置编译期排好 |
| 是否需要指令匹配代码 | **编译期**（TemplateCompilationMode） | 否，无指令依赖就不生成 |
| 常量是否被多处共享 | **编译期**（ConstantPool） | 否，已提升为 `_cN` |
| 管道参数是否全字面量（可内联） | **编译期**（generatePureLiteralStructures） | 否，直接内联结果 |
| 连续 `nextContext` 是否可合并 | **编译期**（mergeNextContextExpressions） | 否，已合并为一次调用 |
| **哪个指令匹配到这个元素** | **运行时**（首次创建时匹配，结果缓存进 TView） | **是**（编译期不知道具体实例） |
| 绑定的**值**是多少 | **运行时** | 是 |
| 视图是否需要刷新（脏标记） | **运行时**（signals / markForCheck） | 是 |
| 条件块当前是否显示 | **运行时** | 是 |

观察这张表可以得出一个清晰的结论：**凡是"静态可知"的决策，Angular 全在编译期做了；运行时只留那些真正依赖运行状态的判断（第 12–15 行）。**

这就是为什么 Ivy 的运行时代码那么"直"——它拿到的是一份**已经把所有能算的都算完的指令序列**，只需要按顺序执行。对照虚拟 DOM 方案（运行时要 diff 整棵树、要判断节点类型、要处理各种动态情况），Angular 的快不是偶然，是编译期把工作做完了。

**实战含义**：这也解释了为什么 Angular 的性能优化常常是"改变模板写法"——因为模板写法直接决定了编译产物。比如用 `@for` 的 `track`（影响 `track_fn_optimization`）、避免模板里的函数调用（影响纯函数提取）、拆分组件（影响 DomOnly 和增量构建）。**在 Angular 里做性能优化，本质是在给编译器喂更好的输入。**

---

### 2.18 本章核心结论（续）：阅读回顾与自评

合上讲义前，试着不翻文件回答这四个"一句话问题"：

1. **一句话说清三种编译形态**：JIT / AOT / Partial+Linker 各自的时机、产物和适用对象是什么？
2. **一句话说清流水线**：模板字符串到 `ɵɵ` 指令，中间经历了哪几次 AST 形态变化？
3. **一句话说清 decls/vars**：它们分别由哪个阶段的什么算法算出？
4. **一句话说清 partial compilation 的价值**：为什么它能解决"库与应用版本锁死"？

如果有卡壳，回到对应小节（§2.1 / §2.2 / §2.7.5 / §2.1.3）再读一遍。这四条是本系列后续章节反复用到的承重结构。

#### 2.18.1 一个值得品味的观察：编译器的三次架构演进

把本章的内容和 Angular 的历史放在一起看，有一条清晰的演进脉络：

| 时代 | 架构 | 核心思路 |
| --- | --- | --- |
| View Engine（≤ v8） | 单趟、全量编译 | 编译产物与运行时强耦合，导致库/应用版本锁死 |
| Ivy 早期（v9–v16） | 单趟直接生成代码 | 产物可摇树，但优化空间受限 |
| **IR 流水线（v17+）** | **ingest → 多趟 transform → emit** | 优化可独立增删，74 个阶段各司其职 |

每一次演进都在解决上一代的痛点：**版本耦合 → 用 partial 格式解耦；优化受限 → 用 IR 多趟变换解耦**。

这背后是一以贯之的工程哲学——**把"决策"从"不可逆的时刻"往后挪**。库发布时不做代码生成（挪到应用构建时），IR 到 emit 前都是可变的（挪到所有分析做完之后），常量池用 `FixupExpression` 占位（挪到所有代码生成完之后），槽位号用 `SlotHandle` 间接引用（挪到布局确定之后）。**"能晚点决定的，就别早决定"**——这条原则贯穿了整个 Angular 编译器，也是读它源码时最值得带走的方法论。

---

### 版本与勘误说明

本章所有源码引用、文件路径、常量值、函数行为均以本仓库 `angular@22.2.3` 的实际代码为准，并在写作前逐一核对过对应源文件。需要特别提示两点：

1. **IR 阶段清单是易变区域**。 `phases/` 下的 74 个文件、以及 `emit.ts` 里 `phases` 数组的**顺序和内容**，会随版本增删调整（某个阶段可能被合并、拆分或重排）。本章给出的是 22.2.3 的快照，请以此版本源码为准。
2. `**ɵɵngDeclare***`** 是版本协商的公开契约**。与 `ɵɵ` 指令不同，部分声明格式（ `R3DeclareComponentMetadata` 等）是**长期稳定的契约**——它必须稳定，否则老库就无法被新 linker 链接。因此这方面的描述具有更长的时效性。

若发现与最新源码有出入之处，多半是内部实现（尤其是 IR 阶段）已重构。请以 `goldens/public-api/` 界定的公开契约为准绳，内部实现细节允许随版本演进。

换言之：**本章讲的是"编译器的构造原理"，而不是"某个版本的阶段清单"**。原理稳定，清单易变——把这个区别记住，你就能在任何版本的 Angular 源码里复用本章的方法论。

**延伸阅读建议**：如果你想在本章之后再深入编译器的某一块，推荐按这个顺序自修——先看 `packages/compiler/src/render3/r3_ast.ts`（建立 R3 AST 的完整心智模型），再读 `template/pipeline/src/ingest.ts`（看 AST 是怎么被"翻译"成 IR 的），最后挑 `phases/` 里你最感兴趣的 3–5 个阶段精读。**不要试图一次读完 74 个阶段**——记住 §2.7.8 那条编排脉络，需要时按需查阅即可。编译器源码的正确读法从来不是通读，而是"带着一个问题，追一条线"。

举三个可以马上动手的"追线练习"：

- **追** `**@for**`** 的 track 是怎么优化的** → `phases/track_fn_optimization.ts` + `phases/track_variables.ts`；
- **追** `**@defer**`** 是怎么生成懒加载代码的** → `phases/defer_configs.ts`（导出 `configureDeferInstructions`）+ `phases/resolve_defer_deps_fns.ts` + `render3/r3_deferred_blocks.ts`；
- **追增量水合是怎么插进模板的** → `phases/insertIncrementalHydrationRuntime.ts`（这条线会一直通到第 23 章）。

每条线追通一次，你对编译器的理解就会从"知道有 74 个阶段"升级为"知道它们是如何协作解决一个具体问题的"。

排序方式 :

### **成为第一条评论！**

Nobody's responded to this post yet. Add your thoughts and get the conversation going.

## Sitemap

See the full [sitemap](https://www.angryecho.com/sitemap.md) for all pages.
