原文来自: huy.rocks - TypeScript / How the co…
作者:huy
这篇文章的灵感来源于 Orta Therox 的 How the TypeScript compiler compiles,如果你想深入了解 TypeScript 的编译过程的话,也许你也会有兴趣看看这个。
从比较高的维度上来看,TypeScript 编译器是一个帮助我们分析和编译 TypeScript 代码到 JavaScript (*.js),同时产出类型定义文件 (*.d.ts) 和 source maps (*.js.map) 的工具。
如果 TypeScript 代码中有些错误,TypeScript 的编译器也能够帮助我们确定什么地方出了问题,以及如何解决。
一. 编译过程
在 TypeScript 的内部,编译是一个包括了许多不同步骤的复杂过程,下图绘制的就是 TypeScript 的简要编译过程。
编译过程会在 tsc 命令被使用时启动,在启动编译前,TypeScript 需要一个 tsconfig.json 文件,这个文件主要是为了确定 编译配置 和 输入文件 这两部分
{
"files": [
"src/*.ts"
],
"compilerOptions": {
...
}
}
在开始编译时,编译上下文将被创建为一个 Program 对象,这个方法被定义在 TypeScript 仓库的src/compiler/program.ts 文件中。创建Program 对象后,它将加载所有的输入文件以及输入文件中所 imports 导入的文件。并调用解析器 Parser(定义在 src/compiler/parser.ts 中),将每个文件解析成 AST(抽象语法树)。
在底层,解析器 Parser 会创建一个 Scanner 实例,它将扫描源代码并生成代表 token 种类的SyntaxKind 流(注:比如说我们词法分析得到的一个 token 是 const,此 token 对应的种类即为 SyntaxKind.ConstKeyword, SyntaxKind 用来表示 token 的种类)。
解析过程并没有就此停止,在此之后,AST 将被送入绑定器 Binder(定义在 src/compiler/binder.ts 中),以创建 AST 节点和符号 Symbol 之间的映射。
上图中的符号 Symbol 是用来存储每个 AST 节点 类型信息的元数据。绑定器 Binder 会创建一个 Symbol 表,它将在后面的阶段中被使用,比如类型检查。
在这之后,随着 Program.emit 被调用,Emit Worker 将被创建,并将 AST 转化为 JavaScript 源代码和一些其他的东西。发射器 Emitter 可以分为两类:
- The JavaScript Emitter: 定义在 src/compiler/emitter.ts 中, 用来输出生成的 JavaScript 代码及 Source Maps。
- Type Definition Emitter: 定义在 src/compiler/definitionEmitter.ts 中, 用来输出生成的类型定义文件。
当发射器 Emitter 运行时,它将会调用 getDiagnostics() 函数以创建一个类型检查器 Type Checker,这个类型检查器被定义在 src/compiler/checker.ts 文件中。接下来发射器 Emitter 将会遍历 AST 并处理每个 Node 节点。
在每个节点上,它将执行代码分析,并使用符号表的类型数据,如果一切顺利,最终的 JavaScript 代码将被生成。
二. 错误报告
在编译过程中,可能会有不同类型的错误被抛出,这取决于编译器是在哪个阶段发现错误的。
enum BuildResultFlags {
None = 0,
Success = 1 << 0,
DeclarationOutputUnchanged = 1 << 1,
ConfigFileErrors = 1 << 2,
SyntaxErrors = 1 << 3,
TypeErrors = 1 << 4,
DeclarationEmitErrors = 1 << 5,
EmitErrors = 1 << 6,
AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors
}
例如,如果 tsconfig.json 文件中存在错误,TypeScript 编译器将会抛出 ConfigFileErrors 这个错误。
如果是扫描器 Scanner 发现了一个错误,那么这个错误就属于 SyntaxErrors。有时,代码的语法正确,但语义不正确,在大多数情况下,它们属于 TypeErrors,且可以被解析器 Parser 或 类型检查器 Type Checker 捕获。比如说
let a: number = "hello";
这段代码的语法是正确的,但在语义上是不正确的,因为你不能把一个字符串值分配给一个 number 变量。
三. 总结
在这篇文章中,只简要说明了编译过程中不同部分的概述,以及不同编译部分之间的联系,有了这个,你就能够探索 TypeScript 源代码,看看它实际是如何实现的。
建议阅读 TypeScript Compiler Internals 文档,它比本文更加深入(此文档也深入到了代码部分,以及编译过程中编译器的不同部分是如何相互调用的)。