0. 回顾

上文提到,performCompilation,做了两件事情,
createProgramemitFilesAndReportErrorsAndGetExitStatus

第三、四、五篇文章,我们介绍了 createProgram
它主要在做词法分析、语法分析,最终返回一棵 AST。

上一篇(第六篇),我们开始介绍 emitFilesAndReportErrorsAndGetExitStatus
里面包含了类型检查相关的代码。

本文继续研究 emitFilesAndReportErrorsAndGetExitStatus
挖一下源码,看看 TypeScript 是怎么生成 js 文件的。

1. 灵犀一指:emitSourceFile

把 AST 转换成 js 代码,不是一件简单的事情,
TypeScript 需要遍历 AST 的各个节点,逐个进行处理,
代码逻辑主要放在了 src/compiler/emitter.ts#L5180 中,它有 5180 行。

此外,在进行调试的时候发现,由于 TypeScript 还会处理一些内置 .d.ts 文件,
调试过程被严重干扰了,需找到真正处理源文件 debug/index.ts 的调用过程。

经过仔细的探索,我们发现了一个关键函数,emitSourceFilesrc/compiler/emitter.ts#L3485
把断点停在这里之后,以后的流程才是真正处理 debug/index.ts

下文我们就以这个函数为基础,向上分析调用栈,向下跟进执行过程。
事情会变得简单许多。

emitSourceFile,位于 src/compiler/emitter.ts#L3485

  1. function emitSourceFile(node: SourceFile) {
  2. ...
  3. if (emitBodyWithDetachedComments) {
  4. ...
  5. if (shouldEmitDetachedComment) {
  6. emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
  7. return;
  8. }
  9. }
  10. ...
  11. }

我们把其他断点都去掉,只留下该函数第一行的断点,然后启动调试。
淡如止水 TypeScript (七):代码生成 - 图1

我们把调用栈分成了几个部分,

  1. emitSourceFile
  2. pipelineEmitWithHint
  3. ...
  4. emitFilesAndReportErrorsAndGetExitStatus
  5. performCompilation
  6. ...

之所以把 pipelineEmitWithHintsrc/compiler/emitter.ts#L1217,单独拿出来,是有用意的,
是因为,这个函数才是控制 emit 的枢纽函数。

那么,为什么我不直接在 pipelineEmitWithHint 里面打断点呢?
这是因为,pipelineEmitWithHint 会在处理 debug/index.ts 文件之前,处理其他的 .d.ts 文件。
其他处理过程,并不是我们需要的流程。

因此,我们只能将断点打在 emitSourceFile 这个必经之路上,
再回过头来看它是怎么过来的。

2. 枢纽函数:pipelineEmitWithHint

我们来看调用栈,
淡如止水 TypeScript (七):代码生成 - 图2

  1. emitSourceFile
  2. pipelineEmitWithHint
  3. ...
  4. emitFilesAndReportErrorsAndGetExitStatus
  5. performCompilation
  6. ...

emitFilesAndReportErrorsAndGetExitStatuspipelineEmitWithHint
我认为是暂时不用过多关注的,它只是一堆函数的调用过程。

真正开始执行 emit 逻辑的,是从 pipelineEmitWithHint 开始的,
我们来看,pipelineEmitWithHintsrc/compiler/emitter.ts#L1217

  1. function pipelineEmitWithHint(hint: EmitHint, node: Node): void {
  2. ...
  3. if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
  4. ...
  5. if (hint === EmitHint.Unspecified) {
  6. if (isKeyword(node.kind)) return writeTokenNode(node, writeKeyword);
  7. switch (node.kind) {
  8. ...
  9. case SyntaxKind.Identifier:
  10. return emitIdentifier(<Identifier>node);
  11. ...
  12. case SyntaxKind.VariableStatement:
  13. return emitVariableStatement(<VariableStatement>node);
  14. ...
  15. case SyntaxKind.VariableDeclaration:
  16. return emitVariableDeclaration(<VariableDeclaration>node);
  17. case SyntaxKind.VariableDeclarationList:
  18. return emitVariableDeclarationList(<VariableDeclarationList>node);
  19. ...
  20. }
  21. ...
  22. }
  23. if (hint === EmitHint.Expression) {
  24. switch (node.kind) {
  25. ...
  26. case SyntaxKind.NumericLiteral:
  27. return emitNumericOrBigIntLiteral(<NumericLiteral | BigIntLiteral>node);
  28. ...
  29. }
  30. }
  31. }

它包含了非常多的 case,它有 419 行,
说它是枢纽函数,是因为 pipelineEmitWithHint 会根据 node.kind 分情况调用不同的 emitXXX

3. parse 与 emit 的对应关系

在我们的例子中,debug/index.ts 内容如下,

  1. const i: number = 1;

第四篇中,我们研究了它的解析过程,可粗略表示如下,

  1. parseList
  2. parseDeclaration
  3. parseVariableStatement
  4. parseVariableDeclarationList
  5. parseVariableDeclaration
  6. parseIdentifierOrPattern
  7. parseIdentifier
  8. parseTypeAnnotation
  9. parseType
  10. parseInitializer
  11. parseAssignmentExpressionOrHigher
  12. parseSemicolon

其中,解析过程与 emit 过程,有一种微妙的对应关系,

  1. parseVariableStatement -> emitVariableStatement
  2. parseVariableDeclarationList -> emitVariableDeclarationList
  3. parseVariableDeclaration -> emitVariableDeclaration
  4. parseIdentifier -> emitIdentifier
  5. ...

这的确反应了一些事实,解析器将 TypeScript 源码结构化,得到了一个易于分析的数据结构(AST),
然后,emitter 处理这个数据结构,递归的分节点进行翻译。

4. emit 过程

看清楚了 parse 与 emit 的对应关系之后,整个 emit 流程就很清楚了,
代码首先执行到枢纽函数 pipelineEmitWithHint,开始 emitSourceFile

emitSourceFile,位于 src/compiler/emitter.ts#L3485

  1. function emitSourceFile(node: SourceFile) {
  2. ...
  3. if (emitBodyWithDetachedComments) {
  4. ...
  5. if (shouldEmitDetachedComment) {
  6. emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
  7. return;
  8. }
  9. }
  10. ...
  11. }

它调用了 emitSourceFileWorkersrc/compiler/emitter.ts#L3560

  1. function emitSourceFileWorker(node: SourceFile) {
  2. ...
  3. emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index);
  4. ...
  5. }

接着调用 emitList,然后一系列调用之后,又回到了 pipelineEmitWithHint

  1. pipelineEmitWithHint
  2. ...
  3. emitList
  4. ...
  5. emitSourceFile
  6. pipelineEmitWithHint
  7. ...

淡如止水 TypeScript (七):代码生成 - 图3

再回到 pipelineEmitWithHint 之后,它会根据 node.kind 分情况分析,
接着开始调用 emitVariableStatementsrc/compiler/emitter.ts#L2519

  1. function emitVariableStatement(node: VariableStatement) {
  2. emitModifiers(node, node.modifiers);
  3. emit(node.declarationList);
  4. writeTrailingSemicolon();
  5. }

淡如止水 TypeScript (七):代码生成 - 图4

就这样来回往复,实际上是在递归的处理 AST 的子节点,
紧接着又调用了 emitVariableDeclarationListsrc/compiler/emitter.ts#L2749

  1. function emitVariableDeclarationList(node: VariableDeclarationList) {
  2. writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var");
  3. writeSpace();
  4. emitList(node, node.declarations, ListFormat.VariableDeclarationList);
  5. }

淡如止水 TypeScript (七):代码生成 - 图5

后面的调用过程,就不再详细展开了,此后 TypeScript 又依次调用了,
emitVariableDeclarationemitIdentifieremitNumericOrBigIntLiteral

emitVariableDeclarationsrc/compiler/emitter.ts#L2743

  1. function emitVariableDeclaration(node: VariableDeclaration) {
  2. emit(node.name);
  3. emitTypeAnnotation(node.type);
  4. emitInitializer(node.initializer, node.type ? node.type.end : node.name.end, node);
  5. }

emitIdentifiersrc/compiler/emitter.ts#L1808

  1. function emitIdentifier(node: Identifier) {
  2. const writeText = node.symbol ? writeSymbol : write;
  3. writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol);
  4. emitList(node, node.typeArguments, ListFormat.TypeParameters);
  5. }

emitNumericOrBigIntLiteralsrc/compiler/emitter.ts#L1737

  1. function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
  2. emitLiteral(node);
  3. }

整条 emit 链路如下,

  1. emitSourceFile
  2. emitVariableStatement
  3. emitVariableDeclarationList
  4. emitVariableDeclaration
  5. emitIdentifier
  6. emitNumericOrBigIntLiteral

每一个 emit 由 pipelineEmitWithHintsrc/compiler/emitter.ts#L1217 来调度。

5. 翻译示例

emit 完毕后,得到的 js 代码如下,debug/index.js

  1. var i = 1;

const 为示例,我们来看一下,TypeScript 到底是怎样将它翻译成 var 的。

执行这个操作的代码位置,其实上文中已经提到了,emitVariableDeclarationListsrc/compiler/emitter.ts#L2749

  1. function emitVariableDeclarationList(node: VariableDeclarationList) {
  2. writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var");
  3. writeSpace();
  4. emitList(node, node.declarations, ListFormat.VariableDeclarationList);
  5. }

emitVariableDeclarationList 时,会判断 isVarConst,结果为 false
于是 writeKeyword 就会写入 var

淡如止水 TypeScript (七):代码生成 - 图6

淡如止水 TypeScript (七):代码生成 - 图7


总结

本文介绍了 TypeScript 的生成 js 代码的过程,是由多个 emitXXX 函数互相调用组成,
每一个 emitXXX 接受 AST 子节点作为参数,翻译一小段代码,最终拼凑出整个 js 目标文件。

写入文件时,只是读取所有 emitXXX 的翻译结果,
是在 printSourceFileOrBundlesrc/compiler/emitter.ts#L479,这个函数中完成的,

  1. function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) {
  2. ...
  3. writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles);
  4. ...
  5. }

这个 writer.getText()src/compiler/utilities.ts#L3496,只是返回了已经拼凑完毕的 js 结果,

  1. export function ...(newLine: string): EmitTextWriter {
  2. ...
  3. return {
  4. ...
  5. getText: () => output,
  6. ...
  7. };
  8. }

淡如止水 TypeScript (七):代码生成 - 图8

这就是 TypeScript 根据 AST 生成 js 文件的整个过程了。

参考

TypeScript v3.7.3