浅析TypeScript编译器原理
大厂技术坚持周更精选好文
前言
2 的语言,在运行的 JavaScript 是这样决定的较弱的类型类型不得,能够在及时提示可用的方法,更能开发大型匹配项目的开发和维护。
那TypeScript到底是如何工作的呢,这里面涉及TypeScript编译器的相关原理了!
关键部分
-
扫描仪扫描器:词法分析,生成令牌流
-
Parser解析器:生成AST
-
绑定器:创建连接器AST,形成Symbol
-
Checker检查器:类型检查
-
Emitter 发射器:输出编译后的文件
处理流程
浅析TypeScript编译器原理-
对于源代码,TS首先对它进行词法分析,通过扫描仪进行逐词扫描,生成token
-
解析器对扫描器进行生成并生成一棵树
-
binder会生成符号(),并为AST上的每一个节点绑定上相应的符号
-
checker 检查处理后的 AST,利用其语法进行检查
-
发射器根据最终的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 这个源码的同学可以在摸清整个编译过程中的部分,之后再慢慢研究设计,不然什么巨大的代码量,随便人想弃坑:唏嘘: