学习背景
最近想做一个工具:可以将项目中的 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]-->