本文首先介绍 TS 架构的各个组成,然后是涉及的数据结构,最后会介绍整个编译过程。
原文: Architectural Overview
架构分层
TS 架构层次如上图所示,下面将对每层进行分析。
核心编译器
核心编译器位于最底层,它包含以下部分:
- 语法解析器(Parser):根据 TS 语法,从一系列源文件生成对应的抽象语法树(AST)。
- 类型联合器(Binder):合并同一类型名称的所有声明,例如在不同文件中的同名接口,这使得类型系统可以直接使用合并后的类型。
- 类型检查器(Checker):解析每种类型结构,检查语义并生成恰当的检查结果。
- 代码生成器(Emitter):把
.ts
和.d.ts
文件转换成.js
、.d.ts
和.map
等文件。 - 预处理器(Pre-processor):编译上下文(Compilation)指的是和程序相关的所有文件。编译器会按序检查所有传入的待编译入口文件,然后会把这些文件中直接或间接
import
的文件,以及/// <reference path=... />
指向的文件,纳入到编译过程,从而构成了最终的编译上下文。通过遍历文件索引图,会得到一个已排序的源文件列表,这些文件列表就构成了整个应用程序。在解析import
时,编译器会优先查找.ts
和.d.ts
文件,以确保处理的是最新的文件。编译器默认使用跟 Node.js 类似的模块定位方式,它会逐路径往上查找能匹配到指定模块名的.ts
或.d.ts
文件。如果没有定位到对应的模块,编译器也不一定抛出错误,因为该模块可能在环境模块中被声明了,比如path
等 Node.js 内置模块。
独立编译器
独立编译器在核心编译器的基础上额外提供了批量编译命令,它能针对不同引擎(如 Node.js)采取不同的文件读写策略。我们通过 npm i typescript -g
后,获得的 tsc
命令实际就是这个独立编译器。它会处理我们命令行中指定的文件,然后送入核心编译器进行编译。
语言服务
语言服务为核心编译器封装了一层接口,尤其适用于编辑器一类的应用。
语言服务支持典型的编辑器操作,包括:
- 自动补全、函数签名提示、格式化和高亮、着色等
- 基本重构功能,如重命名
- 调试接口助手,如断点验证
- TS 特有的增量编译(
--watch
)
语言服务被设计用来专门处理这样的场景:在长时间存在的编译上下文中,源文件会随着时间不断的变化。
从这个角度来说,相比于市面上的其他编译器接口,语言服务在对待程序和源码的处理方式上提供了较为不同视角。
附: 详细的语言服务 API 使用文档
独立服务器
独立服务器 tsserver
对编译器和语言服务层进行了封装,对外暴露了一种基于 JSON 协议的接口,称为语言服务协议(LSP)。
附:详细的独立服务器文档
VS Code 就是一个典型的使用语言服务的编辑器,它通过 LSP 来和语言服务通信,从而实现良好的编码体验。
数据结构
TS 编译器中使用到的主要数据结构有以下 6 类:
-
Node
: AST 的基本构建单元块。通常来说,Node
代表了语法中的非终端节点。与非终端节点相对的终端节点,比如标识符、字面量等,也在 AST 中。 -
SourceFile
:对应源文件的 AST 。SourceFile
本身是一个Node
,它额外提供了一些接口,用于访问包括原始文本、文件包含的引用、标识符列表,以及字符位置映射。 -
Program
:编译单元的所有SourceFile
和编译选项的集合。它是类型系统和代码生成的主要入口。 -
Symbol
:已命名的声明,由类型联合器所生成。它连接了 AST 中的声明节点和其他地方的同名声明实体。它是语义系统的基本构建单元块。 -
Type
: 它是语义系统的另一部分,它可以是具名的(如类、接口),也可以是匿名的(如对象字面量)。 -
Signature
: TS 语言中包含三种类型签名:函数调用签名、构造函数签名和索引签名。
编译过程概述
整个编译过程从预处理开始。
一、 预处理器会找出所有 import
语句 和 reference
指令所依赖的文件,并把它们都列为待编译文件。
二、 解析器解析所有待编译文件,生成 AST Node
。这仅仅是以树的形式来抽象表示待编译文件。SourceFile
对象除了是表示文件的 AST 外,还有额外的信息,比如文件名、源码等。不过此时的 SourceFile
并没有包含类型信息。
三、 类型联合器遍历 AST ,生成并绑定 Symbol
。每个具名实体类型都会创建一个 Symbol
。要注意的是,不同的多个声明节点可能有相同的类型名称。这就意味着不用的 Node
可以有相同的 Symbol
,每个 Symbol
会跟踪所有跟它有关的 Node
。举例来说,对于相同名称的 class
和 interface
,它们的类型会合并,并且指向相同的 Symbol
。类型联合器也会处理好作用域,以确保每个 Symbol
处于正确的作用域范围内。
四、 生成 Symbol
之后,通过调用 createSourceFile
就可以生成具有 Symbol
的 SourceFile
了。不过,Symbol
表示的是单个文件中的具名实体类型,由于来自多个文件的同名类型声明可以合并,因此下一步需要通过 Program
对象来构建一个囊括所有文件的全局 Symbol
视图。
五、 Program
使用 createProgram
接口生成,它包括所有 SourceFile
以及 CompilerOptions
。
六、 针对 Program
创建一个 TypeChecker
,它是 TS 类型系统的核心。它主要负责理清来自多个文件的 Symbol
之间的关系,绑定 Type
到 Symbol
,以及生成语义诊断信息(比如错误信息)。具体来说,TypeChecker
做的第一件事是把来自不同 SourceFile
的 Symbol
整合到单个视图中,然后会创建一张 Symbol
表,用来记录所有的 Symbol
,来自不同文件的同名 Symbol
会在这个记录过程中完成合并。一旦 TypeChecker
完成初始化,它就可以处理关于当前 Program
的任何类型问题了,比如:
* 某个 `Node` 的 `Symbol` 是什么?
* 某个 `Symbol` 的 `Type` 是什么?
* AST 的某个局部有哪些 `Symbol` 是可见的?
* 某个函数声明有哪些可用的 `Signature` ?
* 某个文件应该报告哪些错误信息?
TypeChecker
的所有检查都是延迟计算的。如果问它一个问题,它只会检查与这个问题相关的必要信息。也就是说,它会只检查与当前问题相关的 Node
、Symbol
和 Type
,而不会尝试检查额外的信息。
七、 最后,针对 Program
也会创建一个代码生成器,它负责针对给定的 SourceFile
生成预期的代码文件,包括 .js
、.jsx
、.d.ts
和 .js.map
文件。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。