TypeScript 架构概述

本文首先介绍 TS 架构的各个组成,然后是涉及的数据结构,最后会介绍整个编译过程。

原文:
Architectural Overview

架构分层

《TypeScript 架构概述》

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 命令实际就是这个独立编译器。它会处理我们命令行中指定的文件,然后送入核心编译器进行编译。

语言服务

语言服务为核心编译器封装了一层接口,尤其适用于编辑器一类的应用。
语言服务支持典型的编辑器操作,包括:

  1. 自动补全、函数签名提示、格式化和高亮、着色等
  2. 基本重构功能,如重命名
  3. 调试接口助手,如断点验证
  4. 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 语言中包含三种类型签名:函数调用签名、构造函数签名和索引签名。

编译过程概述

《TypeScript 架构概述》

整个编译过程从预处理开始。

一、 预处理器会找出所有 import 语句 和 reference 指令所依赖的文件,并把它们都列为待编译文件。

二、 解析器解析所有待编译文件,生成 AST Node 。这仅仅是以树的形式来抽象表示待编译文件。SourceFile 对象除了是表示文件的 AST 外,还有额外的信息,比如文件名、源码等。不过此时的 SourceFile 并没有包含类型信息。

三、 类型联合器遍历 AST ,生成并绑定 Symbol 。每个具名实体类型都会创建一个 Symbol。要注意的是,不同的多个声明节点可能有相同的类型名称。这就意味着不用的 Node 可以有相同的 Symbol,每个 Symbol 会跟踪所有跟它有关的 Node 。举例来说,对于相同名称的 classinterface ,它们的类型会合并,并且指向相同的 Symbol 。类型联合器也会处理好作用域,以确保每个 Symbol 处于正确的作用域范围内。

四、 生成 Symbol 之后,通过调用 createSourceFile 就可以生成具有 SymbolSourceFile 了。不过,Symbol 表示的是单个文件中的具名实体类型,由于来自多个文件的同名类型声明可以合并,因此下一步需要通过 Program 对象来构建一个囊括所有文件的全局 Symbol 视图。

五、 Program 使用 createProgram 接口生成,它包括所有 SourceFile 以及 CompilerOptions

六、 针对 Program 创建一个 TypeChecker ,它是 TS 类型系统的核心。它主要负责理清来自多个文件的 Symbol 之间的关系,绑定 TypeSymbol,以及生成语义诊断信息(比如错误信息)。具体来说,TypeChecker 做的第一件事是把来自不同 SourceFileSymbol 整合到单个视图中,然后会创建一张 Symbol 表,用来记录所有的 Symbol ,来自不同文件的同名 Symbol 会在这个记录过程中完成合并。一旦 TypeChecker 完成初始化,它就可以处理关于当前 Program 的任何类型问题了,比如:

* 某个 `Node` 的 `Symbol` 是什么?
* 某个 `Symbol` 的 `Type` 是什么?
* AST 的某个局部有哪些 `Symbol` 是可见的?
* 某个函数声明有哪些可用的 `Signature` ?
* 某个文件应该报告哪些错误信息?

TypeChecker 的所有检查都是延迟计算的。如果问它一个问题,它只会检查与这个问题相关的必要信息。也就是说,它会只检查与当前问题相关的 NodeSymbolType ,而不会尝试检查额外的信息。

七、 最后,针对 Program 也会创建一个代码生成器,它负责针对给定的 SourceFile 生成预期的代码文件,包括 .js.jsx.d.ts.js.map 文件。

    原文作者:mingzhong
    原文地址: https://segmentfault.com/a/1190000013660316
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞