学习背景
最近想做一个工具:可以将项目中的 vue template 写法转换为 composition api 写法(之后完工了会产出文档并开源😄)。这第一步就必须要去理解学习 Vue 源码中的 parseHTML 方法,搞懂它是怎么解析 template 转换成 render 函数的。
但是一上来,就被下面一堆正则表达式给搞蒙圈了......
// Regular Expressions for parsing tags and attributes
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute =
/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
以上的正则表达式就是用来匹配template内的标签、属性等元素,之后再通过解析后的结果组成render函数。之前我跟很多同学一样,在看到比较长的正则之后基本就失去了读下去的耐心,这是不好的,在学习中,越痛苦的时候,就是进步越大越快的时候。
正则表达式常用规则
简写符号
| 元字符 | 说明 |
|---|---|
| . | 等价于[^\n\r\u2028\u2029],通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外 |
| \w | 等价于[0-9a-zA-Z_],匹配字母或数字或下划线或汉字,记忆方式:w是word的简写,也称单词字符。 |
| \W | 等价于[^0-9a-zA-Z_]。非单词字符 |
| \s | 等价于[\t\v\n\r\f], 匹配任意的空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是space character的首字母。 |
| \S | [^ \t\v\n\r\f]。 非空白符。 |
| \d | 等价于[0-9]。匹配数字,记忆方式:其英文是digit(数字)。 |
| \D | 就是[^0-9],表示除数字外的任意字符。 |
| \b | 匹配单词的开始或结束 |
| 匹配字符串的开始 | |
| $ | 匹配字符串的结束 |
量词
| 语法 | 说明 |
|---|---|
| * | 重复零次或更多次,等价于{0,} |
| + | 重复一次或更多次,等价于{1,} |
| ? | 重复零次或一次,等价于{0,1} |
| {n} | 重复n次 |
| {n,} | 重复n次或更多次 |
| {n,m} | 重复n到m次 |
区间
| 语法 | 说明 |
|---|---|
| [x|y] | 匹配 x 或 y,可为字符串 |
| [xyz] | 匹配 x 或 y 或 z 的字符 |
| [^xyz] | 匹配 非 x 或 y 或 z 的字符 |
| [a-z] | 匹配 a 到 z 的任意小写字符 |
分组
| 语法 | 说明 |
|---|---|
| (pattern) | 匹配 pattern 并获取该匹配 |
| (?:pattern) | 非捕获分组,匹配 pattern 但不获取该匹配 |
| x(?=pattern) | 向前匹配,获取pattern前面的为x的值 |
| x(?!pattern) | 非向前匹配,获取pattern前面的不为x的值 |
| (?<=pattern)x | 向后匹配,获取pattern后面的为x的值 |
| (?<!pattern)x | 非向后匹配,获取pattern前面的不为x的值 |
对于 ?= 和 ?<= 不理解的看下下面的例子就懂了:
'xxx_love_study_1.mp4'.replace('xxx', '❤️') // ❤️_love_study_1.mp4
// ?=
'xxx_love_study_1.mp4'.replace(/(?=xxx)/g, '❤️') // ❤️xxx_love_study_1.mp4\
// ?<=
'xxx_love_study_1.mp4'.replace(/(?<=xxx)/g, '❤️') //xxx❤️_love_study_1.mp4\
而我接下来要做的就是借助 iHateRegex 以及各式各样的文档来逐一攻破了。
attritube
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
顾名思义,它的作用就是用来匹配标签中各式各样的属性的,比如说:
<div id="mydiv" class="myClass" @click="myClick">
像以上的id="mydiv"、class="myClass"、@click="myClick",它都可以提取出来。
分解其结构:
^\s*: 这个就比较简单,^在这里表示匹配开头,\s表示空白符(space character的首字母),*表示出现任意次,连起来就是从0到无穷多的空白符([^\s"'<>\/=]+): 最后的+代表至少一次,然后[^\s"'<>\/=]表示除了\s,",',<,>,/,=之外所有的字符, 连起来就是出现至少一次除了刚刚那几个之外的字符,这里其实就是匹配的属性名,比如id,class等等(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))??:表示括号内的是非捕获分组,可以不用看\s*(=)\s*: 主要就是匹配个=符号?:"([^"]*)"+: 匹配双引号里面的内容?:表示非捕获分组": 匹配"符号([^"]*): 除了"符号之外的任意字符,出现任意次数"+: 匹配"符号,至少一次
'([^']*)'+: 匹配单引号里面的内容': 匹配'符号([^']*): 除了'符号之外的任意字符,出现任意次数'+: 匹配'符号,至少一次
([^\s"'=<>`]+): 除了空白符号,",',=,<,>之外的字符,出现至少一次?: 三个分组中匹配 0 次或 1 次
dynamicArgAttribute
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
dynamicArgAttribute 主要匹配的事动态属性,例如
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
分解其结构:
^\s*: 如上, 空白字符开头((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*): 匹配=号前面的内容v-[\w-]+:|@|:|#:v-: 以v-开头[\w-]+:\w表示包括下划线在内的单个字符,[\w-]+表示\w以及-匹配至少一次:|@|:|#::或@或#
\[[^=]+?\][^\s"'<>\/=]*\[[^=]+?\]:\[转译符匹配[,[^=]+?表示除了=之外的字符至少一次并且+?表示尽量少的重复,\]则匹配][^\s"'<>\/=]*: 除了空白符号,",',<,>,=,/之外的字符,出现至少一次
(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?: 跟上面attritube一致, 不再重复
ncname
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
先看下unicodeRegExp:
// Unicode 字符集, 表示一系列合法字符
export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
而 unicodeRegExp.source 用于拿到正则表达式 unicodeRegExp 的字符串。
所以 ncname 就是表示一系列合法字符的集合。
qnameCapture
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
这个就是用来匹配cname字符, 看下面的代码主要匹配的形式就是xxxx或者xxxx:xxxx,但是这里为啥要加\\来转译\字符,我就不是太理解了。😂
startTagOpen
const startTagOpen = new RegExp(`^<${qnameCapture}`)
这个就是用来匹配HTML的开始标签的,根据上面的意思就是匹配<xxx或者<xxx:xxx的,而后者的具体内容可以查看 HTML xmlns 属性
startTagClose
const startTagClose = /^\s*(\/?)>/
表示开始标签的结尾,(\/?)>表示0或者1个/,加上>, 其实就是/>或者>, 因为有可能存在<div />这种的标签。
endTag
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
^<\\/: 表示以</为开头[^>]*: 表示除了>的字符,任意数量
这个比较简单,就是匹配结束标签, 例如</div>
doctype
const doctype = /^<!DOCTYPE [^>]+>/i
/^<!DOCTYPE: 表示以<!DOCTYPE开头[^>]+: 表示除了>的字符,出现至少一次
匹配如: <!DOCTYPE html>
comment
const comment = /^<!\--/
这个看似就匹配 <!-- 开头的注释符,但是为何要在第一个-前加转译符\,确实也没太理解 The /^<!--/ regExp causes bug when injecting js to html #7298
conditionalComment
const conditionalComment = /^<!\[/
用来匹配条件注释,类似于
<!--[if IE 8]>
.... some HTML here ....
<![endif]-->