ng

浅析TypeScript编译器原理

2022-03-03  本文已影响0人  程序员阿远

大厂技术坚持周更精选好文

前言

2 的语言,在运行的 JavaScript 是这样决定的较弱的类型类型不得,能够在及时提示可用的方法,更能开发大型匹配项目的开发和维护。

那TypeScript到底是如何工作的呢,这里面涉及TypeScript编译器的相关原理了!

关键部分

处理流程

浅析TypeScript编译器原理
  1. 对于源代码,TS首先对它进行词法分析,通过扫描仪进行逐词扫描,生成token

  2. 解析器对扫描器进行生成并生成一棵树

  3. binder会生成符号(),并为AST上的每一个节点绑定上相应的符号

  4. checker 检查处理后的 AST,利用其语法进行检查

  5. 发射器根据最终的AST生成JS代码和声明文件(d.ts)

扫描仪扫描器

什么是令牌

这里的不同标记和平时的标记实际上是一个东西,这里是用来标记的。扫描器根据生成的标记源代码进行词法分析,每个“标记”的不同类别的标记(标记),实际上是对“词”的一个分类过程。

比如const a = 1;这个行代码,里面有const关键字,有变量a,有数字1,有标志结束;这些每个都可以生成一个token,只是类别不同。

TS 编译器内部枚举类型的所有类型都可以在类型中找到。

类似的类型,还存储了 AST 这个类型,这个类型是解析器的类型。

export const enum SyntaxKind {
        Unknown,
        EndOfFileToken,
        SingleLineCommentTrivia,
        MultiLineCommentTrivia,
        NewLineTrivia,
        WhitespaceTrivia,
        ShebangTrivia, more pleasant manner.
        ConflictMarkerTrivia,
        NumericLiteral,
        BigIntLiteral,
        StringLiteral,
        JsxText,
        JsxTextAllWhiteSpaces,
        //...(more)
}

处理

介绍scanner的工作流程,我想先跟大家介绍几个scanner关于之前处理的函数。等等鉴定。

字符代码

export const enum CharacterCodes {
        _ = 0x5F,
        $ = 0x24,

        _0 = 0x30,
        _1 = 0x31,
        _2 = 0x32,
        _3 = 0x33,
        _4 = 0x34,
        _5 = 0x35,
        _6 = 0x36,
        _7 = 0x37,
        _8 = 0x38,
        _9 = 0x39,

        a = 0x61,
        b = 0x62,
        c = 0x63,
        d = 0x64,
        e = 0x65,
        f = 0x66,
        g = 0x67,
        h = 0x68,
        //...(more)
}

TS 编译器的汇编也是unicode 编码的。在类型中,通过枚举中unicode 中的所有编码方式列出来,为什么要这样做呢?不能通过数值的方式理解什么字符。

文字鉴定

扫描仪中的识别字符都是基于CharacterCodes的。

是否判断是空格

    export function isWhiteSpaceLike(ch: number): boolean {
        return isWhiteSpaceSingleLine(ch) || isLineBreak(ch);
    }

判断是换行符

    export function isLineBreak(ch: number): boolean {
        // ES5 7.3:
        // The ECMAScript line terminator characters are listed in Table 3.
        //     Table 3: Line Terminator Characters
        //     Code Unit Value     Name                    Formal Name
        //     \u000A              Line Feed               <LF>
        //     \u000D              Carriage Return         <CR>
        //     \u2028              Line separator          <LS>
        //     \u2029              Paragraph separator     <PS>
        // Only the characters in Table 3 are treated as line terminators. Other new line or line
        // breaking characters are treated as white space but not as line terminators.

        return ch === CharacterCodes.lineFeed ||
            ch === CharacterCodes.carriageReturn ||
            ch === CharacterCodes.lineSeparator ||
            ch === CharacterCodes.paragraphSeparator;
    }

确定是数字

    function isDigit(ch: number): boolean {
        return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
    }

除了这些,这里还有很多适合判断的功能,这里就不一一列举了,有兴趣的同学自己查查原因。

(标识符)鉴定

isUnicodeIdentifierStart分别用什么 ,判断比判断是否有某些特征可以作为 TS 编译器的isUnicodeIdentifierPart性质、性质可以分别作为性质来区分。

/* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierStart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierStart);
}

function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierPart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierPart);
}

是否可以作为一个判断允许的情况,并没有通用的规律,ES的一个手动指定的,属性可以做中查的基本是:记录做具体的规定,然后表。

不同的是,只是除了特定的符号外,可能有些特殊的发现在 unicode 编码表中都是连续的,比如显示的

浅析TypeScript编译器原理
const unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170/*...(more)*/ ]
const unicodeESNextIdentifierPart = [48, 57, 65, 90, 95/*...(more)*/ ]

扫描仪用目录的形式记录下可以记录中表的字符的段落,例如 65 是 unicode 编码表的,90 是 Z 是 unicode 编码的位置位置,整个数字的奇数位开始的段位置,偶数位记录了一段段的结束位置。只记录了一段段的所有内容和结尾部分,比该段的所有内容更节省了记录内存。

当需要查找一个字符是否符合规范时,则不同查找方法查询。

function lookupInUnicodeMap(code: number, map: readonly number[]): boolean {
    // Bail out quickly if it couldn't possibly be in the map.
    if (code < map[0]) {
        return false;
    }

    // Perform binary search in one of the Unicode range maps
    let lo = 0;
    let hi: number = map.length;
    let mid: number;

    while (lo + 1 < hi) {
        mid = lo + (hi - lo) / 2;
        // mid has to be even to catch a range's beginning
        mid -= mid % 2;
        if (map[mid] <= code && code <= map[mid + 1]) {
            return true;
        }

        if (code < map[mid]) {
            hi = mid;
        }
        else {
            lo = mid + 2;
        }
    }

    return false;
}

索引

第二个索引要记录一个字符的位置有索引,记录一个是记录一个字符的索引,是记录字符的行列信息和存储的信息,只需要一个方法之前,输出时需要计算有多少个行行,因此需要列行信息;代表存储信息,则代表若有换用各有优劣。

TS编译器采用索引存储信息的方式,并做了一定的优化:每一行第一个字符的索引,使索引转换为行列信息时更加高效。

计算每一行第一个字符的索引,建立表

export function computeLineStarts(text: string): number[] {
    const result: number[] = new Array();
    let pos = 0;
    let lineStart = 0;
    while (pos < text.length) {
        const ch = text.charCodeAt(pos);
        pos++;
        switch (ch) {
            case CharacterCodes.carriageReturn:
                if (text.charCodeAt(pos) === CharacterCodes.lineFeed) {
                    pos++;
                }
            // falls through
            case CharacterCodes.lineFeed:
                result.push(lineStart);
                lineStart = pos;
                break;
            default:
                if (ch > CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) {
                    result.push(lineStart);
                    lineStart = pos;
                }
                break;
        }
    }
    result.push(lineStart);
    return result;
}

通过索引表查询行列号

export function computePositionOfLineAndCharacter(lineStarts: readonly number[], line: number, character: number, debugText?: string, allowEdits?: true): number {
    if (line < 0 || line >= lineStarts.length) {
        if (allowEdits) {
            // Clamp line to nearest allowable value
            line = line < 0 ? 0 : line >= lineStarts.length ? lineStarts.length - 1 : line;
        }
        else {
            Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`);
        }
    }

    const res = lineStarts[line] + character;
    if (allowEdits) {
        // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead)
        // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and
        // apply them to the computed position to improve accuracy
        return res > lineStarts[line + 1] ? lineStarts[line + 1] : typeof debugText === "string" && res > debugText.length ? debugText.length : res;
    }
    if (line < lineStarts.length - 1) {
        Debug.assert(res < lineStarts[line + 1]);
    }
    else if (debugText !== undefined) {
        Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline
    }
    return res;
}

主体流程

仿佛将有可能会以某种方式解析出每个人的源代码,如果全部都保存的话,只需要专注于这段代码的文字,而大量的文字。单独读入整篇文章再理解其中的英文。

TS编译器扫描是扫描scan()个信息,然后再调用一次scan(),逐个获取token的信息。

浅析TypeScript编译器原理

以上为扫描仪工作主流程的函数调用关系,其中的核心函数是扫描函数

function scan(): SyntaxKind {
    startPos = pos; // 记录扫描之前的位置
    while (true) {
        // 这是一个大循环
        // 如果发现空格、注释,会重新循环(此时重新设置 tokenPos,即让 tokenPos 忽略了空格)
        // 如果发现一个标记,则退出函数
        tokenPos = pos;
        // 到字符串末尾,返回结束的token
        if (pos >= end) {
            return token = SyntaxKind.EndOfFileToken;
        }
        // 获取当前字符的编码
        let ch = codePointAt(text, pos);

        switch (ch) {
            // 接下来就开始判断不同的字符可能并组装token
            case CharacterCodes.exclamation: 
                if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { // 后面是不是“=”
                    if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { // 后面是不是还是“=”
                        return pos += 3, token = SyntaxKind.ExclamationEqualsEqualsToken; // 获得“!==”token
                    }
                    return pos += 2, token = SyntaxKind.ExclamationEqualsToken; // 获得“!=”token
                }
                pos++;
                return token = SyntaxKind.ExclamationToken; //获得“!”token
            case CharacterCodes.doubleQuote:
            case CharacterCodes.singleQuote:
                // ...(略)
        }
    }
}

scan 函数400多行代码,其实做的工作逻辑也比较简单:文字字符串,鉴定不同的字符串组件不同的token。

解析器解析器

源代码基础上,如果想要进行类型或者成JS,将源代码组织性的数据转换成一些可选的代码类型,AST是比较好的选择,既能在节点中存储需要很好的表示的信息也属于从属关系。

主体流程

浅析TypeScript编译器原理

paser解析器的主要是通过生成不同的节点树,组成一个抽象的节点语法。

其中的核心任务就是 parseSourceFileWorker

function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
    const isDeclarationFile = isDeclarationFileName(fileName);
    if (isDeclarationFile) {
        contextFlags |= NodeFlags.Ambient;
    }

    sourceFlags = contextFlags;

    // 扫描
    nextToken();
    //解析token,生成node
    const statements = parseList(ParsingContext.SourceElements, parseStatement);
    Debug.assert(token() === SyntaxKind.EndOfFileToken);
    const endOfFileToken = addJSDocComment(parseTokenNode<EndOfFileToken>());

    //创建AST
    const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags);

    // ...(more)

    return sourceFile;

    function reportPragmaDiagnostic(pos: number, end: number, diagnostic: DiagnosticMessage) {
        parseDiagnostics.push(createDetachedDiagnostic(fileName, pos, end, diagnostic));
    }
}

大概流程是这样的:

node节点创建

让我先来看看一个节点节点信息中都包含着什么信息,一个节点节点节点的基础可以在TS编译器的types.ts中找到相关的定义。可以看到,包含着pos(在源代码中的)开始位置)、end(在源代码中的结束位置)、kind(类型节点,定义于SyntaxKind中)等基础信息

export interface ReadonlyTextRange {
    readonly pos: number;
    readonly end: number;
}

export interface Node extends ReadonlyTextRange {
    readonly kind: SyntaxKind;
    readonly flags: NodeFlags;
    /* @internal */ modifierFlagsCache: ModifierFlags;
    /* @internal */ readonly transformFlags: TransformFlags; // Flags for transforms
    readonly decorators?: NodeArray<Decorator>;           // Array of decorators (in document order)
    readonly modifiers?: ModifiersArray;                  // Array of modifiers
    /* @internal */ id?: NodeId;                          // Unique id (used to look up NodeLinks)
    readonly parent: Node;                                // Parent node (initialized by binding)
    /* @internal */ original?: Node;                      // The original node if this is an updated node.
    /* @internal */ symbol: Symbol;                       // Symbol declared by node (initialized by binding)
    /* @internal */ locals?: SymbolTable;                 // Locals associated with node (initialized by binding)
    /* @internal */ nextContainer?: Node;                 // Next container in declaration order (initialized by binding)
    /* @internal */ localSymbol?: Symbol;                 // Local symbol declared by node (initialized by binding only for exported nodes)
    /* @internal */ flowNode?: FlowNode;                  // Associated FlowNode (initialized by binding)
    /* @internal */ emitNode?: EmitNode;                  // Associated EmitNode (initialized by transforms)
    /* @internal */ contextualType?: Type;                // Used to temporarily assign a contextual type during overload resolution
    /* @internal */ inferenceContext?: InferenceContext;  // Inference context for contextual type
}

我们拿来parseVariableStatement举例看看node节点的创建

function parseVariableStatement(pos: number, hasJSDoc: boolean, decorators: NodeArray<Decorator> | undefined, modifiers: NodeArray<Modifier> | undefined): VariableStatement {
   //生成节点描述信息 
    const declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false);
    //解析分号
    parseSemicolon();
    //创建节点
    const node = factory.createVariableStatement(modifiers, declarationList);
    // Decorators are not allowed on a variable statement, so we keep track of them to report them in the grammar checker.
    node.decorators = decorators;
    //添加节点边界信息
    return withJSDoc(finishNode(node, pos), hasJSDoc);
}

parseVariableDeclarationList生成节点的一些描述信息,例如,将描述类信息作为父参数createVariableStatement中,生成节点节点,最后调用finishNode函数,节点范围(pos,end)

function createVariableStatement(modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList | readonly VariableDeclaration[]) {
    const node = createBaseDeclaration<VariableStatement>(SyntaxKind.VariableStatement, /*decorators*/ undefined, modifiers);
    node.declarationList = isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList;
    node.transformFlags |=
        propagateChildFlags(node.declarationList);
    if (modifiersToFlags(node.modifiers) & ModifierFlags.Ambient) {
        node.transformFlags = TransformFlags.ContainsTypeScript;
    }
    return node;
}

binder 绑定器

binder的主要工作是创建符号(符号变量,与ES6的符号没有关系),并且把符号与AST上的节点关联起来。

符号(符号)

当或定义一个元素、函数时,binder会创建一个(其实符号就是我们会先用一个符号来唯一标识)b将所有的连接,建立符号表。当其他地方一个名称例如变量时,就查表(这个名称所代表的符号)。

binder 调用了以下符号函数,初始化一个符号的信息,SymbolFlags符号标志是个标志,用于符号识别类别(例如:角色域标志 FunctionScopedVariable或 BlockScopedVariable等)。

function Symbol(this: Symbol, flags: SymbolFlags, name: __String) {
    this.flags = flags;
    this.escapedName = name;
    this.declarations = undefined;
    this.valueDeclaration = undefined;
    this.id = undefined;
    this.mergeId = undefined;
    this.parent = undefined;
}

主体流程

浅析TypeScript编译器原理

核心功能:

function bind(node: Node | undefined): void {
    if (!node) {
        return;
    }
    //设置父节点
    setParent(node, parent);
    const saveInStrictMode = inStrictMode;
    bindWorker(node);
    if (node.kind > SyntaxKind.LastToken) {
        const saveParent = parent;
        parent = node;
        const containerFlags = getContainerFlags(node);
        if (containerFlags === ContainerFlags.None) {
            //对子节点进行绑定
            bindChildren(node);
        }
        // ...(more)
}

bind函数先设置当前节点的节点信息,紧接着执行bindWorker,根据调用与调用同节点的函数调用bindChildren,对当前节点的每个子节点进行绑定, bindChildren内部也是通过最后一次调用bind对每个节点节点进行绑定。

在不同的bindXXX函数中,其中的核心函数是declareSymbol(declareModuleMember函数实际上是内部调用了declareSymbol方法)

function declareSymbol(symbolTable: SymbolTable, parent: Symbol | undefined, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags, isReplaceableByMethod?: boolean, isComputedName?: boolean): Symbol {
    Debug.assert(isComputedName || !hasDynamicName(node));

    const isDefaultExport = hasSyntacticModifier(node, ModifierFlags.Default) || isExportSpecifier(node) && node.name.escapedText === "default";

    const name = isComputedName ? InternalSymbolName.Computed
        : isDefaultExport && parent ? InternalSymbolName.Default
        : getDeclarationName(node);

    let symbol: Symbol | undefined;
    if (name === undefined) {
        symbol = createSymbol(SymbolFlags.None, InternalSymbolName.Missing);
    }
    else {

        symbol = symbolTable.get(name);

        if (includes & SymbolFlags.Classifiable) {
            classifiableNames.add(name);
        }

        if (!symbol) {
            symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));
            if (isReplaceableByMethod) symbol.isReplaceableByMethod = true;
        }
        //...(more)
    }

    addDeclarationToSymbol(symbol, node, includes);
   //...(more)

    return symbol;
}

需要说明的是binder会维护符号表,在declareSymbol函数中,会判断符号表中是否有同名的符号,如果没有的话,创建新的符号并加入符号表中;有话中直接从符号表中下一个是调用符号信息addDeclarationToSymbol,这个函数主要进行的工作:1.创建AST节点到符号的连接( node.symbol = symbol;添加) 2.为符号添加一个关于节点的声明(symbol.declarations = appendIfUnique(symbol.declarations, node))

夹带私货:TypeScript AST Viewer

这里夹带一下私货,给大家推荐一个非常好用的网站:TypeScript AST Viewer (ts-ast-viewer.com) [1]

浅析TypeScript编译器原理

可以看到代码,只要在左右两边的编辑区域写上ts,,栏目会生成AST,点击上角的具体情况,左下将创建此节点的节目单显示,栏目会显示显示这个节点的一些信息(包括节点相关信息和符号的信息)

检查员

Checker 的代码了,是整个编译器中最重的部分,我也无法读到一个细节,在这四行就细细地做一个分析。

如何检查

比如const b:number = 1;这一行代码,在AST中的结构如下:

浅析TypeScript编译器原理

我们来看看变量的标识符:

浅析TypeScript编译器原理

在 b 绑定的符号符号中,声明中保存着 b 的声明信息,其中有类型属性,其中的 kind 存着 b 的类型的类型,checker 会去这个 kind 和 Numeric Literal 是否匹配,如果检查不匹配,则调用本地的错误函数生成错误报告。

主体流程

浅析TypeScript编译器原理

开始检查类型的入口函数,其中一个为 DicheckSourceFileWorker 的函数

function checkSourceFileWorker(node: SourceFile) {
    const links = getNodeLinks(node);
    if (!(links.flags & NodeCheckFlags.TypeChecked)) {
        if (skipTypeChecking(node, compilerOptions, host)) {
            return;
        }

        // 语法检查
        checkGrammarSourceFile(node);

        clear(potentialThisCollisions);
        clear(potentialNewTargetCollisions);
        clear(potentialWeakMapSetCollisions);
        clear(potentialReflectCollisions);
        //类型检查
        forEach(node.statements, checkSourceElement);
        checkSourceElement(node.endOfFileToken);

        checkDeferredNodes(node);

        if (isExternalOrCommonJsModule(node)) {
            registerForUnusedIdentifiersCheck(node);
        }

        if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) {
            checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => {
                if (!containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) {
                    diagnostics.add(diag);
                }
            });
        }

    // ...(more)
}

我们在checkSourceFileWorker游戏内有各种各样的检查,先执行了checkGrammarSourceFile语法发现,然后执行checkSourceElement、checkDeferredNodes等对其中的具体节点进行具体检查。节点的类别,执行不同类型节点的检查任务。

function checkSourceElementWorker(node: Node): void {
    if (isInJSFile(node)) {
        forEach((node as JSDocContainer).jsDoc, ({ tags }) => forEach(tags, checkSourceElement));
    }

    const kind = node.kind;
      // ...(more)
    if (kind >= SyntaxKind.FirstStatement && kind <= SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) {
        errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected);
    }
    //根据node类型执行不同的检查函数
    switch (kind) {
        case SyntaxKind.TypeParameter:
            return checkTypeParameter(node as TypeParameterDeclaration);
        case SyntaxKind.Parameter:
            return checkParameter(node as ParameterDeclaration);
        case SyntaxKind.PropertyDeclaration:
            return checkPropertyDeclaration(node as PropertyDeclaration);
        case SyntaxKind.PropertySignature:
            return checkPropertySignature(node as PropertySignature);
       //...(more)
    }
 }

检查之后,通过错误函数报告错误

function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
    //生成单条错误
    const diagnostic = createError(location, message, arg0, arg1, arg2, arg3);
    //加入错误报告中
    diagnostics.add(diagnostic);
    return diagnostic;
}

export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage): DiagnosticWithLocation {
    assertDiagnosticLocation(file, start, length);

        let text = getLocaleSpecificMessage(message);

        if (arguments.length > 4) {
        text = formatStringFromArgs(text, arguments, 4);
    }
    return {
        file,
        start,
        length,

        messageText: text,
        category: message.category,
        code: message.code,
        reportsUnnecessary: message.reportsUnnecessary,
        reportsDeprecated: message.reportsDeprecated
    };
}

发射器

发射代码输出的东西主要是通过AST的JS以及声明文件(d.ts)

主体流程

Emitter的主要流程核心函数为emitFiles

export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): EmitResult {
    const compilerOptions = host.getCompilerOptions();
    const sourceMapDataList: SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined;
    const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined;
    const emitterDiagnostics = createDiagnosticCollection();
    const newLine = getNewLineCharacter(compilerOptions, () => host.getNewLine());
    const writer = createTextWriter(newLine);
    const { enter, exit } = performance.createTimer("printTime", "beforePrint", "afterPrint");
    let bundleBuildInfo: BundleBuildInfo | undefined;
    let emitSkipped = false;
    let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined;

    // Emit each output file
    enter();
    forEachEmittedFile(
        host,
        emitSourceFileOrBundle,
        getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit),
        forceDtsEmit,
        onlyBuildInfo,
        !targetSourceFile
    );
    exit();

    return {
        emitSkipped,
        diagnostics: emitterDiagnostics.getDiagnostics(),
        emittedFiles: emittedFilesList,
        sourceMaps: sourceMapDataList,
        exportedModulesFromDeclarationEmit
    };

可以看到这里创建了三个变量sourceMapDataList、emittedFilesList、emitterDiagnostics,这三个分别需要输出的文件数据:sourceMap、JS代码和声明文件、检查的错误报告

function emitJsFileOrBundle(
    sourceFileOrBundle: SourceFile | Bundle | undefined,
    jsFilePath: string | undefined,
    sourceMapFilePath: string | undefined,
    relativeToBuildInfo: (path: string) => string) {
   // ...(more)
    // 将TS语法转换成js语法
    const transform = transformNodes(resolver, host, factory, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false);

    // ...(more)

    // 创建一个printer
    const printer = createPrinter(printerOptions, {
        // resolver hooks
        hasGlobalName: resolver.hasGlobalName,

        // transform hooks
        onEmitNode: transform.emitNodeWithNotification,
        isEmitNotificationEnabled: transform.isEmitNotificationEnabled,
        substituteNode: transform.substituteNode,
    });

    Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform");
    // 输出JS文件
    printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform.transformed[0], printer, compilerOptions);

    // ...(more)
}

再找一个找,发现了emitJsFileOrBundle与
emitDeclarationFileOrBundle函数,输出JS代码,一个emitJsFileOrBundle的逻辑用途,可以看到两件用于输出的事情:1.对每一个节点做一次transform的操作,将TS语法变成JS语法。2.创建一个打印机,调用printSourceFileOrBundle输出js文件。

总结

TypeScript 代码非常适合,内部的细节量的,我每个人的逻辑篇文章都是推荐的冰山一角,这是因为 TypeScript 设计了比较完善的类型复杂的系统。这里给,是一篇关于类型系统的入门介绍,如果对系统相关设计有兴趣的同学可以看一下:类型系统 [2]

最后是一点建议吧,建议初读 TypeScript Compiler 这个源码的同学可以在摸清整个编译过程中的部分,之后再慢慢研究设计,不然什么巨大的代码量,随便人想弃坑:唏嘘:

上一篇下一篇

猜你喜欢

热点阅读