上一篇文章分析 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);     }   }