本文通过学习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),以及对应的源码实现我们通常所说的解析器。解析器的运行过程在分析解析器的原理之前,我们先通过一个例子来看看解析器的具体功能。举个简单的例子:
上面的代码是一个比较简单的模板,转换成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){//...},//每当一条评论被解析时触发该函数功能。整个过程中,读取模板字符串,使用不同的正则表达式匹配不同的内容,然后触发不同的钩子函数对匹配到的截取片段进行处理。比如开始标签匹配开始标签,触发开始钩子函数。钩子函数处理匹配到的起始标记段,生成标记节点并加入到抽象语法树中。同样以上面的例子为例:
整个解析过程是:当解析到
时,会触发一个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}}