当前位置: 首页 > Web前端 > vue.js

Vue3源码分析(二):AST解析器

时间:2023-03-31 18:42:25 vue.js

上一篇我们从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标签。在”时,标签名丢失,报错,解析器进度提前三个字符,跳过“/>”。如果以“',则为自闭标签,三个字符的扫描位置提前if(s[2]==='>'){emitError(context,ErrorCodes.MISSING_END_TAG_NAME,2)advanceBy(context,3)continue//如果第三个字符位置是英文字符,则解析结束标签}elseif(/[a-z]/i.test(s[2])){parseTag(context,TagType.End,parent)continue}else{//如果不是上面的情况,将被解析为伪注释node=parseBogusComment(context)}//如果标签的第二个字符是小写英文字符,它将被解析为元素标记}elseif(/[a-z]/i.test(s[1])){node=parseElement(context,ancestors)//如果第二个字符是'?',则解析它作为伪注释}elseif(s[1]==='?'){node=parseBogusComment(context)}else{//如果这些情况都不存在,就会报错第一个字符不是合法的标签字符emitError(context,ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,1)}}}//如果解析出上述情况,没有创建对应的节点,则将其解析为文本if(!node){node=parseText(context,mode)}//如果节点是数组,则遍历添加到nodes数组中,否则直接添加if(isArray(node)){for(leti=0;i`,`
`,`


`if(element.isSelfClosing||context.options.isVoidTag(element.tag)){returnelement}//递归解析子节点祖先.push(element)constmode=context.options.getTextMode(element,parent)constchildren=parseChildren(context,mode,ancestors)ancestors.pop()element.children=children//解析结束标签if(startsWithEndTagOpen(context.source,element.tag)){parseTag(context,TagType.End,parent)}else{emitError(context,ErrorCodes.X_MISSING_END_TAG,0,element.loc.start)if(context.source.length===0&&元素.tag.toLowerCase()==='script'){constfirst=children[0]if(first&&startsWith(first.loc.source,'