当前位置: 首页 > 后端技术 > Node.js

【Vue源码】一起来学习Vue模板编译原理(一)-模板生成AST

时间:2023-04-03 14:15:28 Node.js

本文通过学习Vue模板编译原理(一)-模板生成AST来分析Vue源码。预计以后会围绕Vue源码整理一些文章,如下。一起学习Vue双向绑定原理-数据劫持和发布订阅一起学习Vue模板编译原理(一)-Template生成AST一起学习Vue模板编译原理(2)-AST生成Renderstring[一起学Vue虚拟DOM分析——虚拟Dom实现与Dom-diff算法]()这些文章放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏哦。编译过程模板编译是Vue.js的核心部分。Vue编译原理的整体逻辑主要分为三部分,或者可以说分为三步。上下文如下:Step1:templatestringsintoelementASTs(parser)静态节点标记,主要用于虚拟DOM渲染优化(optimizer)Step3:使用elementASTs生成渲染函数代码串(codegenerator)对应Vue源码如下,源码位置在src/compiler/index.htmljsexportconstcreateCompiler=createCompilerCreator(functionbaseCompile(template:string,options:CompilerOptions):CompiledResult{//1.parse,将模板字符串转换为抽象语法树(AST)constast=parse(template.trim(),options)//2.optimize,markstaticnodesonASTif(options.optimize!==false){optimize(ast,options)}//3.generate,抽象语法树(AST)生成renderfunctioncodestringconstcode=generate(ast,options)return{ast,render:code.render,staticRenderFns:code.staticRenderFns}})这篇文档主要讲第一步将模板字符串转换为对象语法树(元素AST),以及对应的源码实现我们通常所说的解析器。解析器的运行过程在分析解析器的原理之前,我们先通过一个例子来看看解析器的具体功能。举个简单的例子:

{{name}}

上面的代码是一个比较简单的模板,转换成AST后是这样的:{tag:"div"类型:1,staticRoot:false,static:false,plain:true,parent:未定义,attrsList:[],attrsMap:{},children:[{标签:“p”类型:1,staticRoot:false,static:false,plain:true,parent:{tag:"div",...},attrsList:[],attrsMap:{},children:[{type:2,text:"{{name}}",static:false,expression:"_s(name)"}]}]}其实AST并不是一个很神奇的东西,不要被它的名字吓倒了。它只是用JS中的一个对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点需要的各种数据。实际上,解析器内部还有几个子解析器,如HTML解析器、文本解析器和过滤器解析器,其中最重要的是HTML解析器。顾名思义,HTML解析器的作用就是解析HTML,在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数和注释钩子函数。我们先来看一下解析器的整体代码结构。源码位置src/compiler/parser/index.jsparseHTML(template,{warn,expectHTML:options.expectHTML,isUnaryTag:options.isUnaryTag,canBeLeftOpenTag:options.canBeLeftOpenTag,shouldDecodeNewlines:options.shouldDecodeNewlines,shouldDecodeNewlinesForHref:options.shouldDecodeNewlinesForHref,shouldKeepComment:options.comments,outputSourceRange:options.outputSourceRange,//每当解析标签的开头时触发此函数start(tag,attrs,unary,start,end){//...},//触发此函数每当解析标签的结尾时end(tag,start,end){//...},//每当解析文本时触发此函数chars(text:string,start:number,end:number){//...},//每当一条评论被解析时触发该函数功能。整个过程中,读取模板字符串,使用不同的正则表达式匹配不同的内容,然后触发不同的钩子函数对匹配到的截取片段进行处理。比如开始标签匹配开始标签,触发开始钩子函数。钩子函数处理匹配到的起始标记段,生成标记节点并加入到抽象语法树中。同样以上面的例子为例:

{{name}}

整个解析过程是:当解析到
时,会触发一个tagstart钩子函数start,处理匹配的分片,生成标签节点并加入到AST中;然后在解析到

时,再次触发钩子函数start,处理匹配的片段,生成一个label节点,作为前一个节点的子节点添加到AST中;然后解析到文本行{{name}},此时触发文本钩子函数chars,对匹配段进行处理,生成可变文本的标签节点(下文会提到可变文本)并添加为子节点前一个节点的AST;然后解析为

,在标签末尾触发钩子函数end;然后继续解析到
,此时再次触发标签末尾的钩子函数end,解析结束。正则匹配模板解析过程会涉及到很多正则匹配。如果知道每个正则的用途,后面分析起来会更方便。那么我们先来看看这些正则表达式。源代码位于src/compiler/parser/index.jsexportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\.|^#/:/^v-|^@|^:|^#/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/gconstdynamicArgRE=/^\[.*\]$/constargRE=/:(.*)$/exportconstbindRE=/^:|^\.|^v-bind:/constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/上面的正则化比较简单,基本上都是用来匹配Vue中的一些自定义语法格式的,比如onRE匹配以@或者v-on开头的属性,forAliasRE匹配v-for中的属性值,比如iteminitems,(item,index)项目。下面是一些专门针对html的正则匹配。源代码位于src/compiler/parser/html-parser.jsconstattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/constdynamicArgAttribute=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/constncname=`[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`constqnameCapture=`((?:${ncname}\\:)?${ncname})`conststartTagOpen=newRegExp(`^<${qnameCapture}`)conststartTagClose=/^\s*(\/?)>/constendTag=newRegExp(`^<\\/${qnameCapture}[^>]*>`)constdoctype=/^]+>/iconstcomment=/^

{{text}}

初始HTML模板:

{{text}}

在第一个循环中,拦截生成一个字符串
,解析为div的开始标签,触发hook函数start,拦截后结果为:

{{text}}

第二次循环时,拦截一个换行符空字符串会触发钩子函数chars,截取结果为:

{{text}}

第三次循环,截取一个字符串

并解析p开始输出tag,触发钩子函数start。拦截后的结果为:{{text}}

第四次循环时,拦截了一个字符串{{name}},解析结果为Variablestring,触发钩子函数chars,拦截resultis:

在第5次循环中,截取了一个字符串

,解析为p结束标签,触发了钩子函数end,截取后的结果为:第六轮循环截取一个换行空字符串,会触发chars钩子函数,截取后结果为:第七轮循环截取一段字符串被解析为div结束标签,触发钩子函数结束。拦截后的结果是:第8次循环,只找到一个空字符串,解析完成,循环结束。现在,HTML解析过程清楚了吗?其实在循环过程中分析记录每一个匹配的segment还是很复杂的,因为截取的segment有很多种,比如:开始标签,比如
结束标签,比如
HTML注释,如DOCTYPE,如条件注释,如commenttext,比如'character各个片段的具体处理这里就不说了。感兴趣的可以直接看源码。文本解析器文本解析器对HTML解析器解析出的文本进行二次处理。文本其实有两种,一种是纯文本,一种是带有变量的文本。如下:Thisisplaintext:这里有一段文字,this是带变量的文本:变量。如果是纯文本,则不需要处理;但如果是带有变量的文本,则需要使用文本解析器进一步解析。因为当使用虚拟DOM渲染带有变量的文本时,需要将变量替换为变量中的值。我们知道,当HTML解析器遇到文本时,会触发chars钩子函数。我们先看看钩子函数是如何区分普通文本和可变文本的。源码位置为:src/compiler/parser/html-parser.jschars(text:string,start:number,end:number){//...letchild:?ASTNodeif(!inVPre&&text!==''&&(res=parseText(text,delimiters))){child={type:2,expression:res.expression,tokens:res.tokens,text}}elseif(text!==''||!children.length||children[children.length-1].text!==''){child={type:3,text}}//...children.push(child)}让我们关注res=parseText(text,delimiters)行,通过条件判断设置不同的类型。实际上type=2表示表达式类型,type=3表示普通文本类型。我们来看看parseText函数具体做了什么。导出函数parseText(text:string,delimiters?:[string,string]):TextParseResult|void{consttagRE=分隔符?buildRegex(delimiters):defaultTagRE//无法匹配变量If(!tagRE.test(text)){return}consttokens=[]constrawTokens=[]letlastIndex=tagRE.lastIndex=0letmatch,index,tokenValue//将匹配的变量循环到表达式中while((match=tagRE.exec(text))){index=match.index//推送文本标记//首先在{{之前添加文本到标记中if(index>lastIndex){rawTokens.push(tokenValue=text.slice(lastIndex,index))tokens.push(JSON.stringify(tokenValue))}//标记令牌constexp=parseFilters(match[1].trim())//使用_stotrimvariables打包//将变量更改为`_s(x)`并将其添加到数组中tokens.push(`_s(${exp})`)rawTokens.push({'@binding':exp})//设置lastIndex,保证正则表达式在下一轮循环中不再重复匹配解析出的文本lastIndex=index+match[0].length}//当所有变量都处理完后,如果右边还有left最后一个变量文本,将文本添加到数组if(lastIndex