0. 回顧
上文提到蚊荣,performCompilation
初狰,做了兩件事情,
createProgram
和 emitFilesAndReportErrorsAndGetExitStatus
互例。
第三奢入、四、五篇文章媳叨,我們介紹了 createProgram
腥光,
它主要在做詞法分析、語(yǔ)法分析糊秆,最終返回一棵 AST武福。
上一篇(第六篇),我們開(kāi)始介紹 emitFilesAndReportErrorsAndGetExitStatus
痘番,
里面包含了類(lèi)型檢查相關(guān)的代碼捉片。
本文繼續(xù)研究 emitFilesAndReportErrorsAndGetExitStatus
平痰,
挖一下源碼,看看 TypeScript 是怎么生成 js 文件的伍纫。
1. 靈犀一指:emitSourceFile
把 AST 轉(zhuǎn)換成 js 代碼宗雇,不是一件簡(jiǎn)單的事情,
TypeScript 需要遍歷 AST 的各個(gè)節(jié)點(diǎn)莹规,逐個(gè)進(jìn)行處理赔蒲,
代碼邏輯主要放在了 src/compiler/emitter.ts#L5180 中,它有 5180
行访惜。
此外嘹履,在進(jìn)行調(diào)試的時(shí)候發(fā)現(xiàn),由于 TypeScript 還會(huì)處理一些內(nèi)置 .d.ts
文件债热,
調(diào)試過(guò)程被嚴(yán)重干擾了砾嫉,需找到真正處理源文件 debug/index.ts
的調(diào)用過(guò)程。
經(jīng)過(guò)仔細(xì)的探索窒篱,我們發(fā)現(xiàn)了一個(gè)關(guān)鍵函數(shù)焕刮,emitSourceFile
,src/compiler/emitter.ts#L3485墙杯,
把斷點(diǎn)停在這里之后配并,以后的流程才是真正處理 debug/index.ts
。
下文我們就以這個(gè)函數(shù)為基礎(chǔ)高镐,向上分析調(diào)用棧溉旋,向下跟進(jìn)執(zhí)行過(guò)程。
事情會(huì)變得簡(jiǎn)單許多嫉髓。
emitSourceFile
观腊,位于 src/compiler/emitter.ts#L3485,
function emitSourceFile(node: SourceFile) {
...
if (emitBodyWithDetachedComments) {
...
if (shouldEmitDetachedComment) {
emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
return;
}
}
...
}
我們把其他斷點(diǎn)都去掉算行,只留下該函數(shù)第一行的斷點(diǎn)梧油,然后啟動(dòng)調(diào)試。
我們把調(diào)用棧分成了幾個(gè)部分州邢,
emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
之所以把 pipelineEmitWithHint
儡陨,src/compiler/emitter.ts#L1217,單獨(dú)拿出來(lái)量淌,是有用意的骗村,
是因?yàn)椋@個(gè)函數(shù)才是控制 emit 的樞紐函數(shù)呀枢。
那么叙身,為什么我不直接在 pipelineEmitWithHint
里面打斷點(diǎn)呢?
這是因?yàn)椋?code>pipelineEmitWithHint 會(huì)在處理 debug/index.ts
文件之前硫狞,處理其他的 .d.ts
文件。
其他處理過(guò)程,并不是我們需要的流程残吩。
因此财忽,我們只能將斷點(diǎn)打在 emitSourceFile
這個(gè)必經(jīng)之路上,
再回過(guò)頭來(lái)看它是怎么過(guò)來(lái)的泣侮。
2. 樞紐函數(shù):pipelineEmitWithHint
我們來(lái)看調(diào)用棧即彪,
emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
從 emitFilesAndReportErrorsAndGetExitStatus
到 pipelineEmitWithHint
,
我認(rèn)為是暫時(shí)不用過(guò)多關(guān)注的活尊,它只是一堆函數(shù)的調(diào)用過(guò)程隶校。
真正開(kāi)始執(zhí)行 emit 邏輯的,是從 pipelineEmitWithHint
開(kāi)始的蛹锰,
我們來(lái)看深胳,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
行,
說(shuō)它是樞紐函數(shù)癣猾,是因?yàn)?pipelineEmitWithHint
會(huì)根據(jù) node.kind
分情況調(diào)用不同的 emitXXX
敛劝。
3. parse 與 emit 的對(duì)應(yīng)關(guān)系
在我們的例子中,debug/index.ts
內(nèi)容如下纷宇,
const i: number = 1;
第四篇中夸盟,我們研究了它的解析過(guò)程,可粗略表示如下像捶,
parseList
parseDeclaration
parseVariableStatement
parseVariableDeclarationList
parseVariableDeclaration
parseIdentifierOrPattern
parseIdentifier
parseTypeAnnotation
parseType
parseInitializer
parseAssignmentExpressionOrHigher
parseSemicolon
其中上陕,解析過(guò)程與 emit 過(guò)程,有一種微妙的對(duì)應(yīng)關(guān)系作岖,
parseVariableStatement -> emitVariableStatement
parseVariableDeclarationList -> emitVariableDeclarationList
parseVariableDeclaration -> emitVariableDeclaration
parseIdentifier -> emitIdentifier
...
這的確反應(yīng)了一些事實(shí)唆垃,解析器將 TypeScript 源碼結(jié)構(gòu)化,得到了一個(gè)易于分析的數(shù)據(jù)結(jié)構(gòu)(AST)痘儡,
然后辕万,emitter 處理這個(gè)數(shù)據(jù)結(jié)構(gòu),遞歸的分節(jié)點(diǎn)進(jìn)行翻譯沉删。
4. emit 過(guò)程
看清楚了 parse 與 emit 的對(duì)應(yīng)關(guān)系之后渐尿,整個(gè) emit 流程就很清楚了,
代碼首先執(zhí)行到樞紐函數(shù) pipelineEmitWithHint
矾瑰,開(kāi)始 emitSourceFile
砖茸。
emitSourceFile
,位于 src/compiler/emitter.ts#L3485殴穴,
function emitSourceFile(node: SourceFile) {
...
if (emitBodyWithDetachedComments) {
...
if (shouldEmitDetachedComment) {
emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
return;
}
}
...
}
它調(diào)用了 emitSourceFileWorker
凉夯,src/compiler/emitter.ts#L3560货葬,
function emitSourceFileWorker(node: SourceFile) {
...
emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index);
...
}
接著調(diào)用 emitList
,然后一系列調(diào)用之后劲够,又回到了 pipelineEmitWithHint
震桶。
pipelineEmitWithHint
...
emitList
...
emitSourceFile
pipelineEmitWithHint
...
再回到 pipelineEmitWithHint
之后,它會(huì)根據(jù) node.kind
分情況分析征绎,
接著開(kāi)始調(diào)用 emitVariableStatement
蹲姐,src/compiler/emitter.ts#L2519,
function emitVariableStatement(node: VariableStatement) {
emitModifiers(node, node.modifiers);
emit(node.declarationList);
writeTrailingSemicolon();
}
就這樣來(lái)回往復(fù)人柿,實(shí)際上是在遞歸的處理 AST 的子節(jié)點(diǎn)柴墩,
緊接著又調(diào)用了 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);
}
后面的調(diào)用過(guò)程江咳,就不再詳細(xì)展開(kāi)了,此后 TypeScript 又依次調(diào)用了隘截,
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
每一個(gè) emit 由 pipelineEmitWithHint
丁侄,src/compiler/emitter.ts#L1217 來(lái)調(diào)度鸿摇。
5. 翻譯示例
emit 完畢后拙吉,得到的 js 代碼如下庐镐,debug/index.js
必逆,
var i = 1;
以 const
為示例名眉,我們來(lái)看一下,TypeScript 到底是怎樣將它翻譯成 var
的福压。
執(zhí)行這個(gè)操作的代碼位置荆姆,其實(shí)上文中已經(jīng)提到了胆筒,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
時(shí)顿痪,會(huì)判斷 isVarConst
,結(jié)果為 false
叠聋,
于是 writeKeyword
就會(huì)寫(xiě)入 var
碌补。
6. 總結(jié)
本文介紹了 TypeScript 的生成 js 代碼的過(guò)程镇匀,是由多個(gè) emitXXX
函數(shù)互相調(diào)用組成幸缕,
每一個(gè) emitXXX
接受 AST 子節(jié)點(diǎn)作為參數(shù)晰韵,翻譯一小段代碼栏尚,最終拼湊出整個(gè) js 目標(biāo)文件译仗。
寫(xiě)入文件時(shí)纵菌,只是讀取所有 emitXXX
的翻譯結(jié)果产艾,
是在 printSourceFileOrBundle
,src/compiler/emitter.ts#L479杠览,這個(gè)函數(shù)中完成的,
function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) {
...
writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles);
...
}
這個(gè) writer.getText()
,src/compiler/utilities.ts#L3496佛点,只是返回了已經(jīng)拼湊完畢的 js 結(jié)果超营,
export function ...(newLine: string): EmitTextWriter {
...
return {
...
getText: () => output,
...
};
}
這就是 TypeScript 根據(jù) AST 生成 js 文件的整個(gè)過(guò)程了不跟。