- [Node] 淡如止水 TypeScript (七):代码生成
- [Node] 淡如止水 TypeScript (零):开篇
- [Node] 淡如止水 TypeScript (十):自动补全
- [Node] 淡如止水 TypeScript (一):准备调试
- [Node] 淡如止水 TypeScript (六):类型检查
- [Node] 淡如止水 TypeScript (三):词法分析
- [Node] 淡如止水 TypeScript (九):通信过程
- [Node] 淡如止水 TypeScript (二):开始编译
- [Node] 淡如止水 TypeScript (四):语法分析
- [Node] 淡如止水 TypeScript (八):进程间通信
0. 回顾
上文提到,performCompilation
,做了两件事情,
createProgram
和 emitFilesAndReportErrorsAndGetExitStatus
。
第三、四、五篇文章,我们介绍了 createProgram
,
它主要在做词法分析、语法分析,最终返回一棵 AST。
上一篇(第六篇),我们开始介绍 emitFilesAndReportErrorsAndGetExitStatus
,
里面包含了类型检查相关的代码。
本文继续研究 emitFilesAndReportErrorsAndGetExitStatus
,
挖一下源码,看看 TypeScript 是怎么生成 js 文件的。
1. 灵犀一指:emitSourceFile
把 AST 转换成 js 代码,不是一件简单的事情,
TypeScript 需要遍历 AST 的各个节点,逐个进行处理,
代码逻辑主要放在了 src/compiler/emitter.ts#L5180 中,它有 5180
行。
此外,在进行调试的时候发现,由于 TypeScript 还会处理一些内置 .d.ts
文件,
调试过程被严重干扰了,需找到真正处理源文件 debug/index.ts
的调用过程。
经过仔细的探索,我们发现了一个关键函数,emitSourceFile
,src/compiler/emitter.ts#L3485,
把断点停在这里之后,以后的流程才是真正处理 debug/index.ts
。
下文我们就以这个函数为基础,向上分析调用栈,向下跟进执行过程。
事情会变得简单许多。
emitSourceFile
,位于 src/compiler/emitter.ts#L3485,
function emitSourceFile(node: SourceFile) {
...
if (emitBodyWithDetachedComments) {
...
if (shouldEmitDetachedComment) {
emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
return;
}
}
...
}
我们把其他断点都去掉,只留下该函数第一行的断点,然后启动调试。

我们把调用栈分成了几个部分,
emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
之所以把 pipelineEmitWithHint
,src/compiler/emitter.ts#L1217,单独拿出来,是有用意的,
是因为,这个函数才是控制 emit 的枢纽函数。
那么,为什么我不直接在 pipelineEmitWithHint
里面打断点呢?
这是因为,pipelineEmitWithHint
会在处理 debug/index.ts
文件之前,处理其他的 .d.ts
文件。
其他处理过程,并不是我们需要的流程。
因此,我们只能将断点打在 emitSourceFile
这个必经之路上,
再回过头来看它是怎么过来的。
2. 枢纽函数:pipelineEmitWithHint
我们来看调用栈,

emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
从 emitFilesAndReportErrorsAndGetExitStatus
到 pipelineEmitWithHint
,
我认为是暂时不用过多关注的,它只是一堆函数的调用过程。
真正开始执行 emit 逻辑的,是从 pipelineEmitWithHint
开始的,
我们来看,pipelineEmitWithHint
,src/compiler/emitter.ts#L1217,
function pipelineEmitWithHint(hint: EmitHint, node: Node): void {
...
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
...
if (hint === EmitHint.Unspecified) {
if (isKeyword(node.kind)) return writeTokenNode(node, writeKeyword);
switch (node.kind) {
...
case SyntaxKind.Identifier:
return emitIdentifier(<Identifier>node);
...
case SyntaxKind.VariableStatement:
return emitVariableStatement(<VariableStatement>node);
...
case SyntaxKind.VariableDeclaration:
return emitVariableDeclaration(<VariableDeclaration>node);
case SyntaxKind.VariableDeclarationList:
return emitVariableDeclarationList(<VariableDeclarationList>node);
...
}
...
}
if (hint === EmitHint.Expression) {
switch (node.kind) {
...
case SyntaxKind.NumericLiteral:
return emitNumericOrBigIntLiteral(<NumericLiteral | BigIntLiteral>node);
...
}
}
}
它包含了非常多的 case,它有 419
行,
说它是枢纽函数,是因为 pipelineEmitWithHint
会根据 node.kind
分情况调用不同的 emitXXX
。
3. parse 与 emit 的对应关系
在我们的例子中,debug/index.ts
内容如下,
const i: number = 1;
第四篇中,我们研究了它的解析过程,可粗略表示如下,
parseList
parseDeclaration
parseVariableStatement
parseVariableDeclarationList
parseVariableDeclaration
parseIdentifierOrPattern
parseIdentifier
parseTypeAnnotation
parseType
parseInitializer
parseAssignmentExpressionOrHigher
parseSemicolon
其中,解析过程与 emit 过程,有一种微妙的对应关系,
parseVariableStatement -> emitVariableStatement
parseVariableDeclarationList -> emitVariableDeclarationList
parseVariableDeclaration -> emitVariableDeclaration
parseIdentifier -> emitIdentifier
...
这的确反应了一些事实,解析器将 TypeScript 源码结构化,得到了一个易于分析的数据结构(AST),
然后,emitter 处理这个数据结构,递归的分节点进行翻译。
4. emit 过程
看清楚了 parse 与 emit 的对应关系之后,整个 emit 流程就很清楚了,
代码首先执行到枢纽函数 pipelineEmitWithHint
,开始 emitSourceFile
。
emitSourceFile
,位于 src/compiler/emitter.ts#L3485,
function emitSourceFile(node: SourceFile) {
...
if (emitBodyWithDetachedComments) {
...
if (shouldEmitDetachedComment) {
emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
return;
}
}
...
}
它调用了 emitSourceFileWorker
,src/compiler/emitter.ts#L3560,
function emitSourceFileWorker(node: SourceFile) {
...
emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index);
...
}
接着调用 emitList
,然后一系列调用之后,又回到了 pipelineEmitWithHint
。
pipelineEmitWithHint
...
emitList
...
emitSourceFile
pipelineEmitWithHint
...

再回到 pipelineEmitWithHint
之后,它会根据 node.kind
分情况分析,
接着开始调用 emitVariableStatement
,src/compiler/emitter.ts#L2519,
function emitVariableStatement(node: VariableStatement) {
emitModifiers(node, node.modifiers);
emit(node.declarationList);
writeTrailingSemicolon();
}

就这样来回往复,实际上是在递归的处理 AST 的子节点,
紧接着又调用了 emitVariableDeclarationList
,src/compiler/emitter.ts#L2749,
function emitVariableDeclarationList(node: VariableDeclarationList) {
writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var");
writeSpace();
emitList(node, node.declarations, ListFormat.VariableDeclarationList);
}

后面的调用过程,就不再详细展开了,此后 TypeScript 又依次调用了,
emitVariableDeclaration
,emitIdentifier
,emitNumericOrBigIntLiteral
。
emitVariableDeclaration
,src/compiler/emitter.ts#L2743,
function emitVariableDeclaration(node: VariableDeclaration) {
emit(node.name);
emitTypeAnnotation(node.type);
emitInitializer(node.initializer, node.type ? node.type.end : node.name.end, node);
}
emitIdentifier
,src/compiler/emitter.ts#L1808,
function emitIdentifier(node: Identifier) {
const writeText = node.symbol ? writeSymbol : write;
writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol);
emitList(node, node.typeArguments, ListFormat.TypeParameters);
}
emitNumericOrBigIntLiteral
,src/compiler/emitter.ts#L1737,
function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
emitLiteral(node);
}
整条 emit 链路如下,
emitSourceFile
emitVariableStatement
emitVariableDeclarationList
emitVariableDeclaration
emitIdentifier
emitNumericOrBigIntLiteral
每一个 emit 由 pipelineEmitWithHint
,src/compiler/emitter.ts#L1217 来调度。
5. 翻译示例
emit 完毕后,得到的 js 代码如下,debug/index.js
,
var i = 1;
以 const
为示例,我们来看一下,TypeScript 到底是怎样将它翻译成 var
的。
执行这个操作的代码位置,其实上文中已经提到了,emitVariableDeclarationList
,src/compiler/emitter.ts#L2749,
function emitVariableDeclarationList(node: VariableDeclarationList) {
writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var");
writeSpace();
emitList(node, node.declarations, ListFormat.VariableDeclarationList);
}
emitVariableDeclarationList
时,会判断 isVarConst
,结果为 false
,
于是 writeKeyword
就会写入 var
。


6. 总结
本文介绍了 TypeScript 的生成 js 代码的过程,是由多个 emitXXX
函数互相调用组成,
每一个 emitXXX
接受 AST 子节点作为参数,翻译一小段代码,最终拼凑出整个 js 目标文件。
写入文件时,只是读取所有 emitXXX
的翻译结果,
是在 printSourceFileOrBundle
,src/compiler/emitter.ts#L479,这个函数中完成的,
function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) {
...
writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles);
...
}
这个 writer.getText()
,src/compiler/utilities.ts#L3496,只是返回了已经拼凑完毕的 js 结果,
export function ...(newLine: string): EmitTextWriter {
...
return {
...
getText: () => output,
...
};
}

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