前奏
在紧张的一个星期的整理,笔者的前端小组每个人都整理了一篇文章,笔者整理了Vue编译模版到虚拟树
的思想这一篇幅。建议读者看到这篇之前,先点击这里预习一下整个流程的思想和思路。
本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。
从笔者的 Vue编译思想详解一文中,我们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不做赘述,附上之前的一张图。
本文则旨在从思想落实到源代码分析,当然只是针对parse
这一部分的。
一、 源码结构。
笔者先列出我们在看源码之前,需要先预习的一些概念和准备。
准备
1.正则
parse的最终目标是生成具有众多属性的astElement树,而这些属性有很多则摘自标签的一些属性。 如 div上的v-for、v-if、v-bind等等,最终都会变成astElement的节点属性。 这里先给个例子:
<div v-for="(item,index) in options" :key="item.id"></div>
到
{alias:"item"attrsList:[],attrsMap:{"v-for":"(item,index)inoptions",:key:"item.id"},children:(2)[{…},{…}],end:139,for:"options",iterator1:"index",key:"item.id",parent:{type:1,tag:"div",attrsList:Array(0),attrsMap:{…},rawAttrsMap:{…},…},plain:false,rawAttrsMap:{v-for:{…},:key:{…}},start:15,tag:"div",type:1,}
可以看到v-for的属性已经被解析和从摘除出来,存在于astElement的多个属性上面了。而摘除
的这个功能就是出自于正则强大的力量。下面先列出一些重要的正则预热。
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/
正则基础不太好的同学可以先学两篇正则基础文章,特别详细:
轻松入门正则表达式
正则一条龙
并且附带上两个网站,供大家学习正则。
正则测试
正则图解
一次性看到这么多正则是不是有点头晕目眩。不要慌,这里给大家详细讲解下比较复杂的几条正则。
1)获取属性的正则
attribute 和 dynamicArgAttribute 分别获取普通属性和动态属性的正则表达式。 普通属性大家一定十分熟悉了,这里对动态属性做下解释。
动态属性,就是key值可能会发生变动的属性,vue的写法如 v-bind:[attrName]="attrVal"
,通过改变attrName来改变传递的属性的key值。(非动态属性只能修改val值)。
我们先对attribute
这个通用正则做一个详细的讲解:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
很长对不对??
但是细细的拆分的化,一共五个分组。
1.([^\s"'<>/=]+)
这个分组是匹配 非空格、"、'、<、>、/、= 等符号的字符串。 主要会匹配到属性的key值部分。如下面的属性:
id="container"
([^\s"'<>/=]+)会匹配到id。
2.\s*(=)\s* 这个是 匹配 = 号,当然了空格页一并匹配了。比如下面的属性:
id="container"id="container"
都会匹配到 = 号,第二个会把空格一起匹配了。
3."([^"])"正则一 、'([^'])'正则二 、([^\s"'=<>`]+)正则三 . 这三个正则分别匹配三种情况 "val" 、'val' 、val。还是继续拿例子来讲。
id="container"//exp1id='container'//exp2id=container//exp3
对于exp1
,正则一
会匹配到"container"
, exp2
,正则2
匹配到'container'
,exp3
的话正则三
会匹配到container。
Vue源码的正则基本将大多数情况都考虑在内了。
这样的话应该比较清晰了,我们来概括下:
attribute匹配的一共是三种情况, name="xxx" name='xxx' name=xxx。
能够保证属性的所有情况都能包含进来。 需要注意的是正则处理后的数组的格式是:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']
下面讲源码的时候,会知道这种数组格式是attr属性的原始状态,parse后期会将这种属性处理成attrMap的形式,大致如下:
{name:'xxx',id:'container'}
关于这个正则,我们附上一个讲解图:
而关于dynamicArgAttribute, 则是大同小异:
主要是多了\[[^=]+\][^\s"'<>\/=]*
也就是 [name] 或者 [name]key 这类情况,附上正则详解图:
2)标签处理正则
标签主要包含开始标签 (如<div>
)和结束标签(如</div>
),正则分别为以下两个:
constncname=`[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`constqnameCapture=`((?:${ncname}\\:)?${ncname})`conststartTagOpen=newRegExp(`^<${qnameCapture}`)conststartTagClose=/^\s*(\/?)>/constendTag=newRegExp(`^<\\/${qnameCapture}[^>]*>`)
能够看到标签的匹配是以qnameCapture为基础的,那么这玩意又是啥呢? 其实qname就是类似于xml:xxx的这类带冒号的标签,所以startTagOpen是匹配<div
或<xml:xxx
的标签。 endTag匹配的是如</div>或</xml:xxx>
的标签
3)处理vue的标签
exportconstonRE=/^@|^v-on:/处理绑定事件的正则exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\.///v-|@click|:name|.stop指令匹配:/^v-|^@|^:/
一眼就能看出来,对不对?直接进入复杂的for标签。
for 标签比较重要,匹配也稍微复杂点,这里做个详解:
exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/
首先申明这里的正则是依赖于attribute正则的,我们会拿到v-for里面的内容,举个例子v-for="item in options"
,我们最终会处理成一个map的形式,大致如下:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/0
也就是说我们会在item in options
的基础上进行正则匹配。 先看forAliasRE
的分组,一共两个分组分别是([\s\S]*?)
和([\s\S]*)
会分别匹配 item
和 options
。这里举的例子比较简单。 实际上 in
或of
之前的内容可能会比较复杂的,如(value,key)
或者(item,index)
等,甚至可能(value,key,index)
,这个时候就是forIteratorRE
开始起作用了。 它一共两个分组都是([^,\}\]]*)
,其实就是拿到alias
的最后两个参数,大家都知道Vue对于Object的循环,是可以这么做的,例子如下:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/1
而forIteratorRE
则是为了获取key
和index
的。最终会放在astElement的iterator1
和 iterator2
。
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/2
好了关于正则就说这么多了,具体的情况还是得自己去看看源码的。
2.源码结构
依然是在开始讲源码前,先大致介绍下源码的结构。先贴个代码出来
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/3
模块一大致是一些功能函数,给出代码:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/4
模块二则是一些parse函数作用域内的全局标志和存储容器,代码如下:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/5
模块三是核心部分,也就是解析template的部分,这个函数一旦执行完, 模块2的root会变成一颗以astElement为节点的dom树。
,其代码大致为:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/6
parseHTML函数和 options 是解析的关键,options包括很多平台配置和 传入的四个处理方法。大致如下:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/7
笔者之前的parse思想的文章,已经介绍过两个处理函数start和end了,一个是创建astElement另一个是建立父子关系,其中细节会在下文中,详细介绍,这也是本文的重点。
chars函数处理的是文本节点,commend处理的则是注释节点。 切记这四个函数至关重要,下面会用代号讲解。
二、各模块重点功能。
Vue的html解析并非一步到位,先来介绍一些重点的函数功能
1.parseHTML函数内部功能函数详细讲解。
(1)解析开始标签和处理属性,生成初始化match。
前面我们说到了startTagOpen
是用来匹配开始标签的。而parseHTML
里面的parseStartTag
函数则是利用该正则,匹配开始标签,创立一种初始的数据结构match,保存相应的属性,对于开始标签里的所有属性,如id、class、v-bind,都会保存到match.attr中。
代码如下:
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/8
上面的while中,我们是用开始标签的结束符作为结束条件的。 startTagClose的正则是
constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?///重要1constdynamicArgAttribute=/^\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=/^<!DOCTYPE[^>]+>/i//#7298:escape-toavoidbeingpasedasHTMLcommentwheninlinedinpageconstcomment=/^<!\--/constconditionalComment=/^<!\[/exportconstonRE=/^@|^v-on:/exportconstdirRE=process.env.VBIND_PROP_SHORTHAND?/^v-|^@|^:|^\./:/^v-|^@|^:/exportconstforAliasRE=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/exportconstforIteratorRE=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/conststripParensRE=/^\(|\)$/g//在v-for中去除括号用的。constdynamicArgRE=/^\[.*\]$///判断是否为动态属性constargRE=/:(.*)$///配置:xxxexportconstbindRE=/^:|^\.|^v-bind:///匹配bind的数据,如果在组件上会放入prop里面否则放在attr里面。constpropBindRE=/^\./constmodifierRE=/\.[^.\]]+(?=[^\]]*$)/gconstslotRE=/^v-slot(:|$)|^#/constlineBreakRE=/[\r\n]/constwhitespaceRE=/\s+/gconstinvalidAttributeRE=/[\s"'<>\/=]/9
它本身除了判断是否已经结束,还有一个\/?
是用来判断是否为一元标签的。 一元标签就是如<img/>
可以只写一个标签的元素。这个标记后面会用到。
parseStartTag的目标是比较原始的,获得类似于
id="container"0
match大致可以概括为获取标签、属性和位置信息。并将此传递给下个函数。
(2)handleStartTag处理parseStartTag传递过来的match。
id="container"1
handleStartTag的本身效果其实非常简单直接,就是吧match的attrs重新处理,因为之前是数组结构,在这里他们将所有的数组式attr变成一个对象,流程大致如下:
从这样:
id="container"2
变成这样:
id="container"3
那么其实还有些特殊处理expectHTML
和 一元标签
。
expectHTML
是为了处理一些异常情况。如 p标签的内部出现div等等、浏览器会特殊处理的情况,而Vue会尽量和浏览器保持一致。具体参考 p标签标准。
最后handleStartTag会调用 从parse传递的start(1)函数来做处理,start函数会在下文中有详细的讲解。
(3) parseEndTag
parseEndTag本身的功能特别简单就是直接调用options传递进来的end函数,但是我们观看源码的时候会发现源码还蛮长的。
id="container"4
看起来还蛮长的,其实主要都是去执行options.end, Vue的源码有很多的代码量都是在处理特殊情况,所以看起来很臃肿。这个函数的特殊情况主要有两种:
1.编写者失误,有标签没有闭合。会直接一次性和检测的闭合标签一起进入options.end。 如:
id="container"5
在处理div的标签时,根据pos的位置,将pos之前的所有标签和匹配到的标签都会一起遍历的去执行end函数。
2.p标签和br标签
可能会遇到</p>
和 </br>
标签 这个时候 p标签会走跟浏览器自动补全效果,先start再end。 而br则是一元标签,直接进入end效果。
2.start、end、comment、chars四大函数。
1)start函数
start函数非常长。这里截取重点部分
id="container"6
1).创建astElement节点。
结构如下:
id="container"7
2)处理属性 当然在这里只是处理部分属性,且分为两种情况:
(1)pre模式 直接摘取所有属性
(2)普通模式 分别处理processFor(element) 、processIf(element) 、 processOnce(element)。
这些函数的详细细节,后文会有讲解,这里只是让大家有个印象。
2)end函数
end函数非常短
id="container"8
end函数第一件事就是取出当前栈的父元素赋值给currentParent,然后执行closeElement,为的就是能够创建完整的树节点关系。 所以closeElement才是end函数的重点。
下面详细解释下closeElement
id="container"9
主要是做了五个操作:
1.processElement。
processElement是closeElement非常重要的一个处理函数。先把代码贴出来。
id="container"id="container"0
可以看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最后一个遍历的执行的transforms。
我们一个个来探讨一下,给大家留个印象,实际上,后面会有案例详细讲解函数们的作用。
1.首先最为简单的是processKey和processRef,在这两个函数处理之前,我们的key属性和ref属性都是保存在astElement上面的attrs和attrsMap,经过这两个函数之后,attrs里面的key和ref会被干掉,变成astElement的直属属性。
2.探讨一下slot的处理方式,我们知道的是,slot的具体位置是在组件中定义的,而需要替换的内容又是组件外面嵌套的代码,Vue对这两块的处理是分开的。
先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。
id="container"id="container"1其次是摘取需要替换的内容,也就是 processSlotContent,这是是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和 slotTarget,也就是目标slot。
processComponent 并不是处理component,而是摘取动态组件的is属性。 processAttrs是获取所有的属性和动态属性。
transforms是处理class和style的函数数组。这里不做赘述了。
2.添加elseif 或else的block。
最终生成的的ifConditions块级的格式大致为:
id="container"id="container"2
这里会将条件展示处理成一个数组,exp存放所有的展示条件,如果是else 则为undefined。
3.处理slot,将各个slot对号入座到一个对象scopedSlots。
processElement完成的slotTarget的赋值,这里则是将所有的slot创建的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候可以方便去使用vm.?slot。有兴趣的童鞋可以去看看vm.$slot的初始化。
4.处理树到父子关系,element.parent = currentParent。
5.postTransforms。
不做具体介绍了,感兴趣的同学自己去研究下吧。
3)chars函数
id="container"id="container"3
chars主要处理两中文本情况,静态文本和表达式,举个例子:
id="container"id="container"4
name就是静态文本,创建的type为3.
id="container"id="container"5
而在这个里面name则是表达式,创建的节点type为2。
做个总结就是:普通tag的type为1,纯文本type为2,表达式type为3。
4)comment函数比较简单
id="container"id="container"6
也是纯文本,只是节点加上了一个isComment:true的标志。
3.核心代码parseHTML内部探索
上面完成了一些重要函数的讲解,下面开始识别器的探索。
我们的主要目的是了解parse的主要目的和过程。不会在一些细枝末节作太多赘述。
1)概览
parseHTML函数的结构如下:
id="container"id="container"7
parseHTML原理是用各个正则,不断的识别并前进的的过程。举个列子:
id="container"id="container"8
startTagOpen会先匹配到<div
,然后index会前进四个位置到4,并将html去掉前面到部分,然后匹配id="xxx"
,index前进了8个位置到了13,空格也会算一个位置,html去掉这一部分。然后匹配text,最后通过endTag正则匹配<div>
。这样就结束了。
当然了,匹配到到结果都是通过各个功能函数去处理。
2)标记
先介绍下各个参数的作用,在详细了解while里面的逻辑。
这里的核心参数一共有stack、index、last、lastTag。
他们贯穿了整个匹配线路,index相信大家已经明白是起什么作用的了。我们这里分析下其他属性的作用域。
1)现看下stack的功能吧:
先看一个示例
id="container"id="container"9
这种误写的情况,如果按顺序识别的话,那么span标签永远不会得到end函数的处理,因为没有识别到闭合标签。所以stack有着检查错误的功能。
stack的处理方式是,识别到开始标签就会推入stack。识别到闭合标签就会把对应的闭合标签推出来。
像上面那种情况,当识别到到时候,我们会发现,stack里面上面到span,下面才是div,我们会把这两个一起处理掉。这样能保证生成的astElement树的结构包括span。
2)last的作用
请大家思考一个问题,什么时候我们才会结束?
其实就是parseHTML函数不起作用了,换句话说就是while绕了一圈发现,index没有变,html也没有变。 剩下的部分,我们会当作文本处理掉。
而这块的逻辑就是:
id="container"//exp1id='container'//exp2id=container//exp30
有没有恍然大悟的感觉? 原来最后一步都是判断中间的处理部分有没有动html。last就是记录处理前的样式,然后在后面对比。没有变动了就只剩下文本了。我们直接当文本处理了。
3.lastTag。
这个标记使用的地方特别多,记录的是上个标签。因为有些特殊的情况,需要判断上个标签。 如p标签,记录了上个标签是lastTag,如果里面出现了div等标签,我们会从:
id="container"//exp1id='container'//exp2id=container//exp31
变成:
id="container"//exp1id='container'//exp2id=container//exp32
原因请参考这里。
3)while循环解析器之剖析
while的轮廓:
id="container"//exp1id='container'//exp2id=container//exp33
笔者将上面的代码大致分为四个模块,我们逐一来分析讲解。
模块一的代码:
id="container"//exp1id='container'//exp2id=container//exp34
模块一是在let textEnd = html.indexOf('<');
的textEnd为0的时候,才进入的。
模块一的主要功能是匹配comment、conditionalComment、doctypeMatch、endTagMatch、startTagMatch五种情况。他们的共同特性是匹配并且处理完后,会调用advance函数进行前进。
不同的是comment、endTagMatch、startTagMatch会分别进入options.comment、options.end和options.start函数。 comment函数比较简单,这里不做赘述来,让我们具体看endTagMatch和startTagMatch。
1) 先看startTag:
id="container"//exp1id='container'//exp2id=container//exp35
parseStartTag函数之前我们有说过,除了匹配还会通过attribute正则摘取所有的属性,并生成一个match对象。 格式如下:
id="container"//exp1id='container'//exp2id=container//exp36
然后把结果交给handleStartTag进行处理。 handleStartTag的功能前面也有说明,主要是将原始的正则匹配到到内容,格式一下:
id="container"//exp1id='container'//exp2id=container//exp37
会变成:
id="container"//exp1id='container'//exp2id=container//exp38
并把类match结构推入到stack当中,最后执行了options.start函数。
2)再看endTag
id="container"//exp1id='container'//exp2id=container//exp39
可以看到匹配到endTag,主要是进入了parseEndTag函数。 前面已经说过,parseEndTag函数主要是判断结束标签,再stack到位置,并把stack尾部到这个位置之间到所有到标签都通过options.end函数处理掉。options.end则使用closeElement去处理各个astElement到父子关系。
模块二
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']0
模块二主要是检查下<符号之后的代码,其中的所有非特殊代码都赋值到text上,换言之,就是不断检查有咩有endTag、startTagOpen、comment等特殊情况,一旦检测到就停止,将前面到可能是文本到部分赋值给text。而text会当作文本信息让模块三去处理。
模块三
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']1
模块三为类文本信息,我们会通过options.chars函数去处理,这个函数则会进一步,判断是否存在表达式文本,就是我们经常绑定到值如:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']2
模块四
这个模块处理到是script或style标签,这里暂且不做赘述了,请大家自行去研究。
三、具体示例探索。
说了太多概念,不免会有些抽象,那么直接给出一个具体的示例吧。
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']3
刚进来到达while流程的是html就是完整的代码:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']4
先通过parseStartTag解析<div class="container" id="root">
,得到的结果为:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']5
我们能看到解析到每个属性,也就是attrs的对象的时候,都会用input去记录还剩下的html。 然后将这个结果交给handleStartTag,去处理。
handleStartTag会将上面的attrs重新加工下,从数组变成:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']6
将相应的参数传递给options.start去处理。这个函数的入参大致如下:
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']7
那么start函数本身呢,就去创建astElement,并处理掉v-for、v-if、v-once几种标签,这几种标签的处理方式,大致相同,从attrs去掉对应的属性,然后直接给astElement本身创建新的属性,下面给出处理后的格式如下:
1.v-if
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']8
猜猜上述对ifConditions的第三个exp的undefined会是什么情况?
其实就是v-else的处理方式。
神秘面纱可以揭开了,关于v-if 、v-else-if 不会同时作为父节点的chidren而存在,而是只有一个children,那就是v-if,然后其他的会存放在ifConditions里面。
那么它们在源码的具体流程是怎么样子的?
['name','=','val','','']或者['name','=','','val','']或者['name','=','','','val']9
2.v-for
那么v-for我们最终会处理成什么样子呢?以及又是这么处理成这种样子的。
如果我们的案例是这样的:
{name:'xxx',id:'container'}0
我们得到的结果会是:
{name:'xxx',id:'container'}1
这里没有牵扯到closeElement了,直接在processFor一步到味,我们详细的看看吧。
{name:'xxx',id:'container'}2
好的,结果出来了。
接着我们对解析案例,我们已经处理了开始标签<div class="container" id="root">
,那么剩下对还有
{name:'xxx',id:'container'}3
那么接下来呢? parseStartTag会匹配到什么呢?
是<div v-if="show">
吗?
不好意思,并不是。现实的template
各个标签之间都有空格,所以在while
循环中,对于<
符号的匹配根本不会为0,所以进不了前面所说到模块一
,而是通过模块二
匹配到下一个<
符号,并判断是否为注释
、开始标签
、结束标签
的一种。 如果是,那么从位置0
到 下一个<
符号之间的字符串
,我们有理由相信这是一个文本节点,交给模块三到options.chars去处理。
很显然,从位置0到,下一个开始标签<div v-if="show">
之间是有很多空格的,我们会生成一个文本空节点。
然后中间的过程我们省略的说吧。
处理<div v-if="show">
处理文本节点show attr bind
处理结束标签
好了,这是我们处理的第一个结束标签 ,我们详细的看看吧。
{name:'xxx',id:'container'}4
到了这里我们还剩下:
{name:'xxx',id:'container'}5
然后继续省略的讲解:
处理<div v-for="(item,index) in options" :key="item.id">
,可以参照上面笔者描述的v-for处理方式看。
处理空节点
处理<span>
开始标签
处理文本标签{{item.id}}
,需要注意的是,expression建立的astElement的type为2。
处理</span>
结束标签
处理空节点
处理<div>
开始标签
处理文本标签{{item.text}}
,type也是2
处理</div>
结束标签,结束处理方式相同。
处理空节点
处理</div>
结束标签,结束处理方式相同。
处理空节点
处理</div>
结束标签,结束处理方式相同。
处理空节点
四、整体流程总结。
普通标签处理流程描述
1.识别开始标签,生成匹配结构match。
{name:'xxx',id:'container'}6
2.处理attrs,将数组处理成 {name:'xxx',value:'xxx'}
3.生成astElement,处理for,if和once的标签。
4.识别结束标签,将没有闭合标签的元素一起处理。
5.建立父子关系,最后再对astElement做所有跟Vue 属性相关对处理。slot、component等等。
文本或表达式的处理流程描述。
1、截取符号<之前的字符串,这里一定是所有的匹配规则都没有匹配上,只可能是文本了。
2、使用chars函数处理该字符串。
3、判断字符串是否含有delimiters,默认也就是${},有的话创建type为2的节点,否则type为3.
注释流程描述
1、匹配注释符号。
2、 使用comment函数处理。
3、直接创建type为3的节点。
完结感言
时间仓促,希望多多支持。