[Node] 淡如止水 TypeScript (七):代码生成

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

function emitSourceFile(node: SourceFile) {
  ...
  if (emitBodyWithDetachedComments) {
    ...
    if (shouldEmitDetachedComment) {
      emitBodyWithDetachedComments(node, statements, emitSourceFileWorker);
      return;
    }
  }
  ...
}

我们把其他断点都去掉,只留下该函数第一行的断点,然后启动调试。


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

emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...

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

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

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

2. 枢纽函数:pipelineEmitWithHint

我们来看调用栈,


emitSourceFile
pipelineEmitWithHint
...
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...

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

真正开始执行 emit 逻辑的,是从 pipelineEmitWithHint 开始的,
我们来看,pipelineEmitWithHintsrc/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;
    }
  }
  ...
}

它调用了 emitSourceFileWorkersrc/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 分情况分析,
接着开始调用 emitVariableStatementsrc/compiler/emitter.ts#L2519

function emitVariableStatement(node: VariableStatement) {
  emitModifiers(node, node.modifiers);
  emit(node.declarationList);
  writeTrailingSemicolon();
}

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

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

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

emitVariableDeclarationsrc/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);
}

emitIdentifiersrc/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);
}

emitNumericOrBigIntLiteralsrc/compiler/emitter.ts#L1737

function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
  emitLiteral(node);
}

整条 emit 链路如下,

emitSourceFile
emitVariableStatement
emitVariableDeclarationList
emitVariableDeclaration
emitIdentifier
emitNumericOrBigIntLiteral

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

5. 翻译示例

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

var i = 1;

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

执行这个操作的代码位置,其实上文中已经提到了,emitVariableDeclarationListsrc/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 的翻译结果,
是在 printSourceFileOrBundlesrc/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 文件的整个过程了。

参考

TypeScript v3.7.3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266