架构概述


层次概述

Architectural overview.

  • 核心TypeScript编译器

    • 语法分析器(Parser): 以一系列原文件开始, 根据语言的语法, 生成抽象语法树(AST)
    • 联合器(Binder): 使用一个Symbol将针对相同结构的声明联合在一起(例如:同一个接口或模块的不同声明,或拥有相同名字的函数和模块)。这能帮助类型系统推导出这些具名的声明。
    • 类型解析器与检查器(Type resolver / Checker): 解析每种类型的构造,检查读写语义并生成适当的诊断信息。
    • 生成器(Emitter): 从一系列输入文件(.ts和.d.ts)生成输出,它们可以是以下形式之一:JavaScript(.js),声明(.d.ts),或者是source maps(.js.map)。
    • 预处理器(Pre-processor): “编译上下文”指的是某个“程序”里涉及到的所有文件。上下文的创建是通过检查所有从命令行上传入编译器的文件,按顺序,然后再加入这些文件直接引用的其它文件或通过import语句和/// <reference path=... />标签间接引用的其它文件。

沿着引用图走下来你会发现它是一个有序的源文件列表,它们组成了整个程序。
当解析导出(import)的时候,会优先选择“.ts”文件而不是“.d.ts”文件,以确保处理的是最新的文件。
编译器会进行与Nodejs相似的流程来解析导入,沿着目录链查找与将要导入相匹配的带.ts或.d.ts扩展名的文件。
导入失败不会报error,因为可能已经声明了外部模块。

  • 独立编译器(tsc): 批处理编译命令行界面。主要处理针对不同支持的引擎读写文件(比如:Node.js)。
  • 语言服务: “语言服务”在核心编译器管道上暴露了额外的一层,非常适合类编辑器的应用。

语言服务支持一系列典型的编辑器操作比如语句自动补全,函数签名提示,代码格式化和突出高亮,着色等。
基本的重构功能比如重命名,调试接口辅助功能比如验证断点,还有TypeScript特有的功能比如支持增量编译(在命令行上使用--watch)。
语言服务是被设计用来有效的处理在一个长期存在的编译上下文中文件随着时间改变的情况;在这样的情况下,语言服务提供了与其它编译器接口不同的角度来处理程序和源文件。

请参考 [[Using the Language Service API]] 以了解更多详细内容。

JS中文网,javascriptC.com,阿里云双11新客上云仅86元/年 答题领亿元补贴

数据结构

  • Node: 抽象语法树(AST)的基本组成块。通常Node表示语言语法里的非终结符;一些终结符保存在语法树里比如标识符和字面量。
  • SourceFile: 给定源文件的AST。SourceFile本身是一个Node;它提供了额外的接口用来访问文件的源码,文件里的引用,文件里的标识符列表和文件里的某个位置与它对应的行号与列号的映射。
  • Program: SourceFile的集合和一系列编译选项代表一个编译单元。Program是类型系统和生成代码的主入口。
  • Symbol: 具名的声明。Symbols是做为联合的结果而创建。Symbols连接了树里的声明节点和其它对同一个实体的声明。Symbols是语义系统的基本构建块。
  • Type: Type是语义系统的其它部分。Type可能被命名(比如,类和接口),或匿名(比如,对象类型)。
  • Signature: 一共有三种Signature类型:调用签名(call),构造签名(construct)和索引签名(index)。

编译过程概述

整个过程从预处理开始。
预处理器会算出哪些文件参与编译,它会去查找如下引用(/// <reference path=... />标签和import语句)。

语法分析器(Parser)生成抽象语法树(AST)Node.
这些仅为用户输出的抽象表现,以树的形式。
一个SourceFile对象表示一个给定文件的AST并且带有一些额外的信息如文件名及源文件内容。

然后,联合器(Binder)处理AST节点,结合并生成Symbols
一个Symbol会对应到一个命名实体。
这里有个一微妙的差别,几个声明节点可能会是名字相同的实体。
也就是说,有时候不同的Node具有相同的Symbol,并且每个Symbol保持跟踪它的声明节点。
比如,一个名字相同的classnamespace可以合并,并且拥有相同的Symbol
联合器也会处理作用域,以确保每个Symbol都在正确的封闭作用域里创建。

生成SourceFile(还带有它的Symbols们)是通过调用createSourceFile API。

到目前为止,Symbol代表的命名实体可以在单个文件里看到,但是有些声明可以从多文件合并,因此下一步就是构建一个全局的包含所有文件的视图,也就是创建一个Program

一个ProgramSourceFile的集合并带有一系列CompilerOptions
通过调用createProgram API来创建Program

通过一个Program实例创建TypeChecker
TypeChecker是TypeScript类型系统的核心。
它负责计算出不同文件里的Symbols之间的关系,将Type赋值给Symbol,并生成任何语义Diagnostic(比如:error)。

TypeChecker首先要做的是合并不同的SourceFile里的Symbol到一个单独的视图,创建单一的Symbol表,合并所有普通的Symbol(比如:不同文件里的namespace)。

在原始状态初始化完成后,TypeChecker就可以解决关于这个程序的任何问题了。
这些“问题”可以是:

  • 这个NodeSymbol是什么?
  • 这个SymbolType是什么?
  • 在AST的某个部分里有哪些Symbol是可见的?
  • 某个函数声明的Signature都有哪些?
  • 针对某个文件应该报哪些错误?

TypeChecker计算所有东西都是“懒惰的”;为了回答一个问题它仅“解决”必要的信息。
TypeChecker仅会检测和这个问题有关的NodeSymbolType,不会检测额外的实体。

对于一个Program同样会生成一个Emitter
Emitter负责生成给定SourceFile的输出;它包括:.js.jsx.d.ts.js.map

Js中文网 – ts中文手册,JavaScript 教程, www.javascriptC.com,typescript 中文文档
一个帮助开发者成长的社区,你想要的,在这里都能找到

术语

完整开始/令牌开始(Full Start/Token Start)

令牌本身就具有我们称为一个“完整开始”和一个“令牌开始”。“令牌开始”是指更自然的版本,它表示在文件中令牌开始的位置。“完整开始”是指从上一个有意义的令牌之后扫描器开始扫描的起始位置。当关心琐事时,我们往往更关心完整开始。

函数 描述
ts.Node.getStart 取得某节点的第一个令牌起始位置。
ts.Node.getFullStart 取得某节点拥有的第一个令牌的完整开始。
琐碎内容(Trivia)

语法的琐碎内容代表源码里那些对理解代码无关紧要的内容,比如空白,注释甚至一些冲突的标记。

因为琐碎内容不是语言正常语法的一部分(不包括ECMAScript API规范)并且可能在任意2个令牌中的任意位置出现,它们不会包含在语法树里。但是,因为它们对于像重构和维护高保真源码很重要,所以需要的时候还是能够通过我们的APIs访问。

因为EndOfFileToken后面可以没有任何内容(令牌和琐碎内容),所有琐碎内容自然地在非琐碎内容之前,而且存在于那个令牌的“完整开始”和“令牌开始”之间。

虽然这个一个方便的标记法来说明一个注释“属于”一个Node。比如,在下面的例子里,可以明显看出genie函数拥有两个注释:

var x = 10; // This is x.

/**
 * Postcondition: Grants all three wishes.
 */
function genie([wish1, wish2, wish3]: [Wish, Wish, Wish]) {
    while (true) {
    }
} // End function

这是尽管事实上,函数声明的完整开始是在var x = 10;后。

我们依据Roslyn’s notion of trivia ownership处理注释所有权。通常来讲,一个令牌拥有同一行上的所有的琐碎内容直到下一个令牌开始。任何出现在这行之后的注释都属于下一个令牌。源文件的第一个令牌拥有所有的初始琐碎内容,并且最后面的一系列琐碎内容会添加到end-of-file令牌上。

对于大多数普通用户,注释是“有趣的”琐碎内容。属于一个节点的注释内容可以通过下面的函数来获取:

函数 描述
ts.getLeadingCommentRanges 提供源文件和一个指定位置,返回指定位置后的第一个换行与令牌之间的注释的范围(与ts.Node.getFullStart配合会更有用)。
ts.getTrailingCommentRanges 提供源文件和一个指定位置,返回到指定位置后第一个换行为止的注释的范围(与ts.Node.getEnd配合会更有用)。

做为例子,假设有下面一部分源代码:

debugger;/*hello*/
    //bye
  /*hi*/    function

function关键字的完整开始是从/hello/注释,但是getLeadingCommentRanges仅会返回后面2个注释:

d e b u g g e r ; / * h e l l o * / _ _ _ _ _ [CR] [NL] _ _ _ _ / / b y e [CR] [NL] _ _ / * h i * / _ _ _ _ f u n c t i o n
                  ↑                                     ↑       ↑                       ↑                   ↑
                  完整开始                              查找      第一个注释               第二个注释     令牌开始
                                                       开始注释

适当地,在debugger语句后调用getTrailingCommentRanges可以提取出/hello/注释。

如果你关心令牌流的更多信息,createScanner也有一个skipTrivia标记,你可以设置成false,然后使用setText/setTextPos来扫描文件里的不同位置。

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 画漫画的程序员 GitHub仓库,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界