【译】TypeScript编译器是如何工作的(简易版)

67 阅读3分钟

原文地址:TypeScript/How the compiler compiles (huy.rocks)

本文灵感源自How the TypeScript compiler compiles,你可以通过观看此视频深入理解TypeScript的编译过程。

宏观上来说,TypeScript编译器(compiler)是一个帮助我们分析和编译TypeScript代码的工具。它将TypeScript代码转换为JavaScript代码(.js),类型声明文件(.d.ts)和source map文件(*.js.map)

tsc-high-level.png

如果在源文件中存在错误,TypeScript编译器也会提供一些诊断信息,据此我们就能知道代码中发生了哪些错误,并且该如何修复它们。

1. 编译过程

编译器内部由不同的部分组成,具有非常复杂编译的流程,下图是对这些过程的总结:

tsc-overview.png

当你输入tsc命令后,编译器便开始了整个编译流程。在编译前,TypeScript编译器需要读取tsconfig.json文件,该文件实际上定义了两个部分:编译器选项(Compiler Options)输入的文件(Input Files)

{
    "files": [
        "src/*.ts"
    ],
    "compilerOptions": {
        ...
    }
}

编译上下文会被创建为Program对象,该对象在src/compiler/program.ts中定义。当Program对象创建完毕,紧接着它会加载所有输入文件(input files)和它们相关的依赖(their imports)。之后会调用Parsersrc/compiler/parser.ts)将文件转换为 AST(Abstract Syntax Tree,抽象语法树)。

tsc-program-to-ast.png

更底层上讲,Parser会创建一个Scanner实例(src/compiler/scanner.ts),它会扫描源代码并且生成SyntaxKind标记的流(stream of SyntaxKind tokens)。

解析没有就此结束,在此之后,AST会被交给Bindersrc/compiler/binder.ts),创建一个AST NodesSymbols的映射。

tsc-binder-symbols.png

每个Symbol都存储着对应Node类型信息的元数据,Binder创建的Symbols Table将会在之后的阶段(类型检查)被使用。

此后,随着Program.emit被调用,将会创建Emit Worker,并且由它来将AST转换为JavaScript代码字符串和其他一些东西。有两种Emitter类型,分别是:

  • JavaScript Emitter:定义在src/compiler/emitter.ts,生成JavaScript代码和Source Map文件。
  • Type Definition Emitter:定义在src/compiler/definitionEmitter.ts,生成类型定义文件。

Emitter运行时会调用getDiagnostics()方法,用来创建Type Checkersrc/compiler/checker.ts)。然后Emitter会遍历AST以处理每个Node

每个Node都会使用Symbols Table中的类型数据进行代码分析,如果一切顺利,最终会生成对应的JavaScript代码。

tsc-emitter.png

2. 错误报告

编译阶段返回的错误取决于编译器发现错误的阶段。

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存在错误,将会返回ConfigFileErrors

如果错误由Scanner触发,那么返回就是SyntaxErrors。有时代码语法正确,但是存在语义上的错误,大多数情况会返回TypeErrors,该错误一般由ParserType Checker捕获,例如:

let a: number = "hello";

该代码语法正确,但是存在语义错误,因为你不能将一个字符串类型赋值给数值类型的变量。

3. 总结

本篇文章仅概述了整个编译过程,还有各个部分之间的关系。至此,你可以深入TypeScript源码,去探究每个过程的具体实现。

我推荐阅读TypeScript Compiler Internals,它是这篇文章的深入版(它深入了每个部分的代码,并且讲解了彼此调用时所发生的事情)。