上一篇文章分析 Babel 编译流程的时候,提到 Babel 会将 JS 代码转换成 AST(抽象语法树)。这种行为是一种通用的行为,无论什么编程语言都会将源代码解析成 AST,AST 不是 Babel 特有的,更不是 JS 特有的 。
为什么要这么做呢?原始的 JS 文件是计算机是无法理解的,计算机也很难直接修改 JS 代码,但是转换成 AST 后,由于 AST 本质上是一组表示程序结构的对象,我们可以通过修改这个对象,间接的实现修改代码的目的。chrome V8 引擎也是这么做的,比起 Bable 更进一步的是,V8 引擎会编译 AST 生成字节码。
Parser的过程分为两步,第一步,词法分析,也就是编译原理中的有限状态机,将一段代码拆分为一个个Token,第二步,语法分析,将Token数组,转换为AST树。
这次我就看一下源码 ,简单分析一下这个过程。
首先看下Babel-Parser的目录结构
主要是个四个文件夹,util,plugins,tokeinzer,parser
入口1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function parse (input: string, options?: Options ): File { if (options?.sourceType === "unambiguous" ) { options = { ...options, }; try { options.sourceType = "module" ; const parser = getParser (options, input); const ast = parser.parse (); return ast; } catch (moduleError) { try { options.sourceType = "script" ; return getParser (options, input).parse (); } catch {} throw moduleError; } } else { return getParser (options, input).parse (); } }
这段代码核心就是通过getParser
方法获取一个parser,然后用获取的parser去进行解析。
我们再来看一下这个getParser
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function getParser (options: ?Options, input: string ): Parser { let cls = Parser ; if (options?.plugins ) { validatePlugins (options.plugins ); cls = getParserClass (options.plugins ); } return new cls (options, input); } const parserClassCache : { [key : string]: Class <Parser > } = {};function getParserClass (pluginsFromOptions: PluginList ): Class <Parser > { const pluginList = mixinPluginNames.filter (name => hasPlugin (pluginsFromOptions, name), ); const key = pluginList.join ("/" ); let cls = parserClassCache[key]; if (!cls) { cls = Parser ; for (const plugin of pluginList) { cls = mixinPlugins[plugin](cls); } parserClassCache[key] = cls; } return cls; }
Parser解析流程到现在,我们搞清楚了入口文件的逻辑,主要就是两部分,第一部分声明Parser,第二部分,如果配置了插件,为Parser开启插件功能。
那我们就继续看一下Parser的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default class Parser extends StatementParser { constructor (options: ?Options, input: string ) { } parse (): File { this .enterInitialScopes (); const file = this .startNode (); const program = this .startNode (); this .nextToken (); file.errors = null ; this .parseTopLevel (file, program); file.errors = this .state .errors ; return file; } }
构造函数中都是一些准备工作,先不关注,主要逻辑还是在这个parse函数中
1.enterInitialScopes1 2 3 4 5 6 7 8 enterInitialScopes ( ) { let paramFlags = PARAM ; if (this .hasPlugin ("topLevelAwait" ) && this .inModule ) { paramFlags |= PARAM_AWAIT ; } this .scope .enter (SCOPE_PROGRAM ); this .prodParam .enter (paramFlags); }
这一步,就是初始化一开始的根节点,以及对应的参数和作用域
2. startNode1 2 3 4 startNode<T : NodeType >(): T { return new Node (this , this .state .start , this .state .startLoc ); }
3. nextToken这部分就是解析的重点了,这一部分的代码会比较复杂,解析过程中会一个个字符得向后解析,利用有限状态机的状态转移去判断不同的状态,最终在到达某种状态,去产生一个token。
如读取到123456这个数字的6时,判断后面是空格或者分号,就生成一个数字的token。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 nextToken (): void { const curContext = this .curContext (); if (!curContext.preserveSpace ) this .skipSpace (); this .state .start = this .state .pos ; if (!this .isLookahead ) this .state .startLoc = this .state .curPosition (); if (this .state .pos >= this .length ) { this .finishToken (tt.eof ); return ; } if (curContext === ct.template ) { this .readTmplToken (); } else { this .getTokenFromCode (this .codePointAtPos (this .state .pos )); } }
4. parseTopLevel1 2 3 4 5 6 7 8 parseTopLevel (file : N.File , program : N.Program ): N.File { file.program = this .parseProgram (program); file.comments = this .state .comments ; if (this .options .tokens ) file.tokens = babel7CompatTokens (this .tokens ); return this .finishNode (file, "File" ); }
这里会继续调用parseProgram
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 parseProgram ( program : N.Program , end : TokenType = tt.eof , sourceType : SourceType = this .options .sourceType , ): N.Program { program.sourceType = sourceType; program.interpreter = this .parseInterpreterDirective (); this .parseBlockBody (program, true , true , end); if ( this .inModule && !this .options .allowUndeclaredExports && this .scope .undefinedExports .size > 0 ) { for (const [name] of Array .from (this .scope .undefinedExports )) { const pos = this .scope .undefinedExports .get (name); this .raise (pos, Errors .ModuleExportUndefined , name); } } return this .finishNode <N.Program >(program, "Program" ); }
再调用parseBlockBody
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 parseBlockBody ( node : N.BlockStatementLike , allowDirectives : ?boolean, topLevel : boolean, end : TokenType , afterBlockParse?: (hasStrictModeDirective: boolean ) => void , ): void { const body = (node.body = []); const directives = (node.directives = []); this .parseBlockOrModuleBlockBody ( body, allowDirectives ? directives : undefined , topLevel, end, afterBlockParse, ); }
继续调用parseBlockOrModuleBlockBody
,最终进入递归,通过parserStatement,next等函数去不断递归调用nextToken,直到将一开始parser方法传入的字符串完全解析完。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 parseBlockOrModuleBlockBody ( body : N.Statement [], directives : ?(N.Directive []), topLevel : boolean, end : TokenType , afterBlockParse?: (hasStrictModeDirective: boolean ) => void , ): void { const oldStrict = this .state .strict ; let hasStrictModeDirective = false ; let parsedNonDirective = false ; while (!this .match (end)) { const stmt = this .parseStatement (null , topLevel); if (directives && !parsedNonDirective) { if (this .isValidDirective (stmt)) { const directive = this .stmtToDirective (stmt); directives.push (directive); if ( !hasStrictModeDirective && directive.value .value === "use strict" ) { hasStrictModeDirective = true ; this .setStrict (true ); } continue ; } parsedNonDirective = true ; this .state .strictErrors .clear (); } body.push (stmt); } if (afterBlockParse) { afterBlockParse.call (this , hasStrictModeDirective); } if (!oldStrict) { this .setStrict (false ); } this .next (); }
总结用一个简单的图来表示,大概就是这样,省略了很多细节
nextToken方法解析 readTmplToken读取模板字符串这是我根据代码分析出来的状态机
我们尝试下看看结果
getTokenFromCode这个函数逻辑上来讲不复杂,但是条件分之特别多,因为需要适配各种不同的字符去判断,简单展示下:
charcodes代码中用到的各种charCodes,是另外一个仓库的内容,这是链接:https://github.com/xtuc/charcodes/blob/master/packages/charcodes/src/index.js
TokenType而finishToken的参数其实是一个个内置好的TokenType,如tt.parentL
其实就是parenL: new TokenType("(", { beforeExpr, startsExpr }),
这些TokenType就是所有babel内置的token类型,而TokenType来源有两个, 一个是Tokenizer内置的,另一种就是parser的plugin提供的,但是我们也说过,parser的plugin对于用户来说只是个开关,所以本质上,所有的TokenType都是babel-praser一开始内置好的。
主要分为四类,一类是变量类型,如number,string,一类是符号,如括号,冒号之类,一类是表达式,如等于,大于,最后就是关键字,如switch,case等
函数逻辑1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 getTokenFromCode (code : number): void { switch (code) { case charCodes.dot : this .readToken_dot (); return ; case charCodes.leftParenthesis : ++this .state .pos ; this .finishToken (tt.parenL ); return ; case charCodes.rightParenthesis : ++this .state .pos ; this .finishToken (tt.parenR ); return ; case charCodes.semicolon : ++this .state .pos ; this .finishToken (tt.semi ); return ; case charCodes.comma : default : if (isIdentifierStart (code)) { this .readWord (code); return ; } } throw this .raise ( this .state .pos , Errors .InvalidOrUnexpectedToken , String .fromCodePoint (code), ); }
parseTopLevel方法解析在一开始的流程解析中,我们要看到了,这个函数的主要逻辑在parseBlockOrModuleBlockBody
函数中,我们就先来看看这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 parseBlockOrModuleBlockBody ( body : N.Statement [], directives : ?(N.Directive []), topLevel : boolean, end : TokenType , afterBlockParse?: (hasStrictModeDirective: boolean ) => void , ): void { const oldStrict = this .state .strict ; let hasStrictModeDirective = false ; let parsedNonDirective = false ; while (!this .match (end)) { const stmt = this .parseStatement (null , topLevel); if (directives && !parsedNonDirective) { if (this .isValidDirective (stmt)) { const directive = this .stmtToDirective (stmt); directives.push (directive); if ( !hasStrictModeDirective && directive.value .value === "use strict" ) { hasStrictModeDirective = true ; this .setStrict (true ); } continue ; } parsedNonDirective = true ; this .state .strictErrors .clear (); } body.push (stmt); } if (afterBlockParse) { afterBlockParse.call (this , hasStrictModeDirective); } if (!oldStrict) { this .setStrict (false ); } this .next (); }
这个函数看起来不短,但是主要逻辑就是while循环,只要满足!this.match(end)
就会一直解析,这个end其实就是tt.eof,也就是我们刚才TokenType中的一种,表示文件结束。
循环体 主要内容就是两个const stmt = this.parseStatement(null, topLevel);
和body.push(stmt);
,这个stmt就是一个AST的Node
parseStatement1 2 3 4 5 6 parseStatement (context : ?string, topLevel?: boolean): N.Statement { if (this .match (tt.at )) { this .parseDecorators (true ); } return this .parseStatementContent (context, topLevel); }
第一行就是判断当前是否是@,如果是,那就是装饰器,这个暂时不管
我们看看这个parseStatementContent
parseStatementContent这个函数就很像刚才的Tokenizer中的getTokenFromCode
了,getTokenFromCode
是根据code生成各种不同类型的token,而parseStatementContent
是根据不同类型的token去生成AST Node。
然后在解析过程中,有些特殊情况,会重新去调用Tokenizer的nextToken继续去生成新的token,比如解析到import
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 parseStatementContent (context : ?string, topLevel : ?boolean): N.Statement { let starttype = this .state .type ; const node = this .startNode (); let kind; if (this .isLet (context)) { starttype = tt._var ; kind = "let" ; } switch (starttype) { case tt._break : case tt._continue : return this .parseBreakContinueStatement (node, starttype.keyword ); case tt._debugger : return this .parseDebuggerStatement (node); case tt._do : return this .parseDoStatement (node); case tt._for : return this .parseForStatement (node); default : { if (this .isAsyncFunction ()) { if (context) { this .raise ( this .state .start , Errors .AsyncFunctionInSingleStatementContext , ); } this .next (); return this .parseFunctionStatement (node, true , !context); } } } const maybeName = this .state .value ; const expr = this .parseExpression (); if ( starttype === tt.name && expr.type === "Identifier" && this .eat (tt.colon ) ) { return this .parseLabeledStatement (node, maybeName, expr, context); } else { return this .parseExpressionStatement (node, expr); } }