上一篇我们从packages/vue/src/index.ts入口开始,了解了一个Vue对象的编译过程。在文章中,我们提到了baseCompile函数在执行过程中,会生成AST抽象语法树。毫无疑问,这是关键的一步,因为只有有了生成的AST,我们才能遍历AST的节点,进行变换操作,比如解析v-if、v-for等指令,或者分析节点到静态提升满足条件的节点,所有节点都依赖于之前生成的AST抽象语法树。那么今天我们就来看看AST的解析,看看Vue是如何解析模板的。生成AST抽象语法树首先我们回顾一下baseCompile函数中的ast相关逻辑及其后续使用:*/constast=isString(模板)?baseParse(template,options):templatetransform(ast,{/*ignoreparameters*/}??)returngenerate(ast,extend({},options,{prefixIdentifiers}))}因为我有Annotatethelogicthatwedon需要注意的是,现在函数体中的逻辑会很清晰:生成一个ast对象,将ast对象作为参数传递给transform函数,对ast节点进行转换操作,将ast对象作为参数传递给generate函数,返回编译结果这里我们主要关注ast的生成。可以看出ast的生成有一个三元运算符的判断。如果传入的模板参数是字符串,则调用baseParse解析模板字符串,否则直接将模板作为ast对象。baseParse中做了什么来生成ast?一起来看看源码,exportfunctionbaseParse(content:string,options:ParserOptions={}):RootNode{constcontext=createParserContext(content,options)//创建解析后的上下文对象conststart=getCursor(context)//生成记录解析过程的游标信息returncreateRoot(//生成并返回根根节点parseChildren(context,TextModes.DATA,[]),//解析子节点为根root的children属性nodegetSelection(context,start))}在baseParse的函数中,我添加了注释,方便大家理解各个函数的作用。首先会创建解析上下文,然后根据上下文获取游标信息。由于还没有解析,游标中的column、line、offset属性对应的都是模板的起始位置。之后,创建根节点并返回根节点。至此,ast树生成完毕,分析完毕。创建AST导出函数createRoot(children:TemplateChildNode[],loc=locStub):RootNode{return{type:NodeTypes.ROOT,children,helpers:[],components:[],directives:[],hoists:[],imports:[],cached:0,temps:0,codegenNode:undefined,loc}}查看createRoot函数的代码,我们可以发现该函数返回一个RootNode类型的根节点对象,其中children我们传入的参数将作为根节点的children参数。这里很好理解,按照树状数据结构想象一下就可以了。所以生成ast的重点将集中在parseChildren函数上。如果不看parseChildren函数的源码,可以大致理解这是一个解析子节点的函数。接下来我们来看看AST解析中最关键的parseChildren函数。这仍然是一个古老的规则。为了帮助大家理解,我将函数体中的逻辑简化一下。解析子节点functionparseChildren(context:ParserContext,mode:TextModes,ancestors:ElementNode[]):TemplateChildNode[]{constparent=last(ancestors)//获取当前节点的父节点constns=parent?parent.ns:Namespaces.HTMLconstnodes:TemplateChildNode[]=[]//存储解析过的节点//当标签没有关闭时,解析对应的节点while(!isEnd(context,mode,ancestors)){/*ignorelogic*/}//处理空白字符,提高输出效率letremovedWhitespace=falseif(mode!==TextModes.RAWTEXT&&mode!==TextModes.RCDATA){/*ignorelogic*/}//去除空白字符并返回解析的节点数组返回removedWhitespace?nodes.filter(Boolean):nodes}从上面的代码我们可以知道parseChildren函数接收三个参数,context:解析器上下文,mode:文本数据类型,ancestors:祖先节点数组。函数执行时,会先从祖先节点获取当前节点的父节点,确定命名空间,创建一个空数组存放解析后的节点。之后会有一个while循环判断是否已经到达标签的关闭位置。如果不是需要关闭的标签,则在循环体中对源模板字符串进行分类分析。之后会有一段处理空白字符的逻辑,处理完成后返回解析出来的nodes数组。在大家对parseChildren的执行过程有了初步的了解之后,我们来看一下函数的核心,while循环中的逻辑。一会儿,解析器会判断文本数据的类型,只有当TextModes为DATA或RCDATA时,才会继续解析。第一种情况是判断是否需要解析Vue模板语法中的“Mustache”语法(双大括号)。如果当前上下文中没有v-pre指令跳过表达式,并且源模板字符串开头被我们指定的字符分隔(此时context.options.delimiters是双大括号),双大括号就会被解析。这里可以发现,如果你有特殊的要求,不想使用双大括号作为表达式插值,那么只需要在编译前更改options中的delimiters属性即可。接下来会判断如果第一个字符是“<”,第二个字符是'!',就会尝试解析comment标签。在”时,标签名丢失,报错,解析器进度提前三个字符,跳过“/>”。如果以“”开头,第三个字符是小写英文字符,解析器将解析结束标签。如果源模板字符串的第一个字符为“<”,第二个字符以小写英文字符开头,则调用parseElement函数解析对应的标签。当判断字符串字符的分支条件结束,没有解析出node节点时,将node作为文本类型,调用parseText进行解析。最后,将生成的节点添加到节点数组中,该数组在函数结束时返回。这是while循环体中的逻辑,也是parseChildren最重要的部分。在这个判断过程中,我们看到了双花括号语法的解析,注释节点是如何解析的,开始标签和结束标签的解析,以及文本内容的解析。简化代码在下方方框内,大家可以对照上面的解释了解源码。当然,源码中的注释也很详细。while(!isEnd(context,mode,ancestors)){consts=context.source让节点:TemplateChildNode|模板子节点[]|undefined=undefinedif(mode===TextModes.DATA||mode===TextModes.RCDATA){if(!context.inVPre&&startsWith(s,context.options.delimiters[0])){/*如果标签没有v-pre指令,源模板字符串以双大括号`{{`开头,按双大括号中括号语法分析*/node=parseInterpolation(context,mode)}elseif(mode===TextModes.DATA&&s[0]==='<'){//如果源模板字符串的第一个字符位置是`!`if(s[1]==='!'){//如果它以'
