什么是AST
在Vue的挂载过程中,模板会被编译成AST语法树。AST指的是抽象语法树,即语法树,它是源代码抽象语法结构的树状表示。
虚拟Dom
Vue的一大特点是通过虚拟DOM模拟DOM对象树来优化DOM操作的技术或思想。
Vue源代码中的虚拟DOM构造通过模板编译成AST语法树——然后转换成render函数,最后返回一个VNode(VNode是Vue的虚拟DOM节点)
本文对源代码中的AST转换部分进行了简单的提取,由于源代码中的转换过程需要通过各种兼容性来判断,非常复杂,所以作者提取了主要的功能代码,用300-400行代码来完成将模板转换为AST的功能。下面用具体代码进行分析。
函数解析(模板){ var currentParent//当前父节点var root//最终返回AST树根节点var stack=[];parseHTML(模板,{ start:函数start(标记,属性,一元)}.},end:函数end(){ 0.},Chars:函数字符(文本){ 0.}})返回根}第一步是调用parse并传入模板,这里假设模板是div id=' app ' span { { message } }/span/div
然后声明三个变量
CurrentParent-存储当前的父元素,root-最终返回AST根节点,stack-使用堆栈来帮助建立树
然后调用parseHTML函数进行转换,传入模板和选项(包括三种方法:开始、结束、字符等。用于解释),然后先看看parseHTML方法
函数parseHTML(html,options){ var stack=[];//这里使用的堆栈数组类似于上面的解析函数,但是这里的堆栈只是简单地存储标签名,以便与结束标签相匹配。var isUnaryTag $ $ 1=isUnaryTag//判断是否为自闭标签;var索引=0;var lastWhile (html) {//第一次进入While循环时,因为字符串以开始,所以进入startTag条件,进行AST转换,最后将对象弹入栈数组last=htmlvar textEnd=html . indexof(' ');If (textEnd===0) {//字符串是否以//endtag:VAR endtag开头匹配=html。match(end tag);if(endTagMatch){ var curIndex=index;高级(endTagMatch[0])。长度);parseEndTag(endTagMatch[1],curIndex,index);Continue} //Start tag: //匹配开始标记var startTagMatch=parsestartag();//处理后得到match if(start tag match){ handle start tag(start tag match);Continue}} //初始化为未定义,安全,字符少一点。VARText=(void 0),REST=(void 0),NEXT=(void 0);如果(textEnd=0) {//截取字符索引=/div在这里,截取封闭的rest=html . slice(textEnd);//截取封闭标签//处理文本中的字符//获取中间字符串={ { message } } text=html . substring(0,text end);//切断关闭标签前的前置(textEnd);//切掉封闭标签的前部}//如果(textEnd 0){字符串不存在时text=htmlhtml=}////处理文本if(options . chars text){ options . chars(text);}}}函数进入while循环获取标签索引var textEnd=html . indexof(' ');如果TEXEND===0,则表示当前是标签xxx或/xxx,然后使用常规匹配查看当前是否是结束标签/xxx。var endTagMatch=html . match(endTag);如果不匹配,则是开始标记。调用parseStartTag()函数进行分析。
函数parseStartTag() {//返回匹配的对象var start=html . match(starttag open);//常规匹配if(start){ var match={ tagname : start[1],//tag name (div) attrs: [],//attribute start 3360 index//cursor index(最初为0)};前进(开始[0])。长度);var end,attrwhile(!(end=html . match(startTagClose))(attr=html . match(attribute)){ advance(attr[0])。长度);match . attrs . push(attr);} if (end) { advance(end[0])。长度);//标记结束位置match.end=index//这里的索引是在parseHTML中定义并提前添加的。return match //返回匹配对象的开始位置和结束位置tagName attrs} }}这个函数主要用于构建一个match对象,它包含tagName(标记名)和attrs(标记属性)。开始(模板中左开始标记的位置),结束(模板中右开始标记的位置),如模板=div id=' app ' div span { { message } }/span/div/div。当程序第一次进入这个函数时,它匹配div标记,所以标记名是divstart:0 end:14,如图所示:
然后返回match作为参数来调用handleStartTag
var startTagMatch=parsestartag();//处理后得到match if(start tag match){ handle start tag(start tag match);继续}接下来,查看函数handleStartTag:
函数handleStartTag(match){ var tagName=match . tagName;Varunary=isunarytag $ $ 1(标记名)//确定它是否是封闭标记var l=match . attrs . length;var attrs=新数组(l);for(var I=0;I l;I){ var args=match . attrs[I];var值=args[3]| | args[4]| | args[5]| ' ';attrs[i]={ name: args[1],value : value };} if(!一元){ stack.push({tag: tagName,lowercacheddag : tagName . tolowercase(),attrs : attrs });lastTag=标记名;} if(options . start){ options . start(tagName,attrs,一元,match.start,match . end);}}功能分为三部分。第一部分是for循环,它转换属性。上一步从parseStartTag()获得的match对象中的attrs属性如图所示
当时的attrs就像上图。通过这个循环,我们将其转换为一个只有两个属性的对象,名称和值,如图:所示
然后判断是否不是自结束标记,将标记名和属性推入栈中(注意这里的变量栈是在parseHTML中定义的,它的作用是存储标记名以便与结束标记匹配。然后调用最后一步的选项. start .这里的选项是我们在Parse函数中调用parseHTML,Parse函数是传入第二个参数的对象(包括start endchars的三个方法函数)。这里我们开始看到选项的功能。
start:函数start(标记,attrs,一元){ var元素={ type: 1,标记:标记,attrsList: attrs,attrsmap : makeAttrsMap(attrs),parent: currentParent,children :[]};processAttrs(元素);if(!root){ root=element;} if(CurrentParent){ CurrentParent . children . push(元素);element.parent=currentParent} if(!一元){ currentParent=elementstack.push(元素);}}在此函数中生成的元素对象连接到元素的父节点和子节点,并最终推送到堆栈
此时,堆栈中的第一个元素如下所示生成:
完成了while循环的第一次执行,进入了第二次循环执行。这时,html变成了span{{message}}/span/div,然后被截取到span处理,第一次通过循环。堆栈中的元素如下所示:
然后继续执行第三个循环。此时,该处理文本节点{{message}}了
//初始化为undefined,安全,字符少一点。vartext=(void 0),rest=(void 0),next=(void 0);如果(textEnd=0) {//截取字符索引=/div在这里,截取封闭的rest=html . slice(textEnd);//截取封闭标签//处理文本中的字符//获取中间字符串={ { message } } text=html . substring(0,text end);//切断关闭标签前的前置(textEnd);//切掉封闭标签的前部}//如果(textEnd 0){字符串不存在时text=htmlhtml=}//另一个函数if(options . chars text){ options . chars(text);}这里的角色是提取文本并调用options.chars函数。接下来,查看options.chars。
chars:函数字符(文本){ if(!CurrentParent) {//如果没有父元素,只是text return } varchildren=current parent . children;//取出孩子//text={ { message } } if(text){ var expression;if (text!==' '(表达式=parse text(text)){//将解析后的文本保存到子数组children中。push ({type: 2,expression3360 expression,text : text });} else if (text!==' ' || !children . length | | children[children . length-1]。文字!==' '){ children . push({ type : 3,text : text });}}}})这里的主要功能是判断文本是{{xxx}}还是简单文本xxx。如果简单文本被推入父元素的子元素,类型被设置为3,如果它是字符模板{{xxx}},则调用parseText转换。比如这里的{{message}}转换为_s(message) (_s是为了下一步AST转换为render函数而添加的,本文暂时不使用。)然后将转换后的内容推送给孩子。
另一个循环已经完成。此时,html=/span/div还有两个结束标记需要匹配
var endTagMatch=html . match(endTag);if(endTagMatch){ var curIndex=index;高级(endTagMatch[0])。长度);parseEndTag(endTagMatch[1],curIndex,index);Continue}接下来,看看parseEndTag函数,它传入标记名开始索引和结束索引
函数parseEndTag(tagName,start,end) { var pos,lowerCasedTagNameif(TagMa){ LowerCadTagMa=TagMa . ToLowerCase();}//如果(标记名){//获取(pos=stack . length-1;pos=0;POS-){//表示没有匹配的标签if(栈[POS]。low rcasedtag==low rcasedtag name){ break } } else {//如果没有提供标记名,clean shop pos=0;}如果(pos=0) { //关闭所有打开的元素,向上堆栈(var I=stack . length-1;i=posI-){ if(options . end){ options . end(stack[I])。标记、开始、结束);} } //从堆栈中移除打开的元素。lastTag=pos stack[pos - 1]。标签;}在这里,首先在堆栈中找到对应开始标记的索引位置,然后调用options.end函数从索引开始到堆栈顶部的所有元素
end:函数end(){//pop stack stack . length-=1;current parent=stack[stack . length-1];},堆叠顶部元素,因为这个元素已经匹配到结束标记,然后更改当前的父元素。最后我把html的内容循环完了,最后返回根的根就是我们想要的AST
这只是Vue的冰山一角。请帮我纠正这篇文章中的任何错误。最近一直在研究Vue的源代码,希望能和大家分享一下我的经验。接下来,我会继续更新后续的源代码。如果你觉得它有帮助,请把它给一个明星
Github地址是:https://github.com/zwStar/vue-ast.欢迎明星还是议题
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。