美文网首页ng
浅析TypeScript编译器原理

浅析TypeScript编译器原理

作者: 程序员阿远 | 来源:发表于2022-03-03 18:33 被阅读0次

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

    前言

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

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

    关键部分

    • 扫描仪扫描器:词法分析,生成令牌流

    • Parser解析器:生成AST

    • 绑定器:创建连接器AST,形成Symbol

    • Checker检查器:类型检查

    • Emitter 发射器:输出编译后的文件

    处理流程

    浅析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));
        }
    }
    
    

    大概流程是这样的:

    • nextToken() 函数执行一次扫描,新的token对旧的token进行解析。

    • parseList函数进行解析,可以看到parseList的第二个参数传了 parseStatement这个函数,这个函数其实是真正的核心执行函数:根据token的类别创建节点节点。

    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编译器原理

    核心功能:

    • bindWorker :根据不同的种类分发不同的bindXXX函数

    • createSymbol :创建符号

    • addDeclarationToSymbol :为节点节点添加声明

    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 这个源码的同学可以在摸清整个编译过程中的部分,之后再慢慢研究设计,不然什么巨大的代码量,随便人想弃坑:唏嘘:

    相关文章

      网友评论

        本文标题:浅析TypeScript编译器原理

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