美文网首页Node.js
[Node] 淡如止水 TypeScript (七):代码生成

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

作者: 何幻 | 来源:发表于2020-01-01 12:03 被阅读0次

    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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/hfiwoctx.html