学习Vue编译源码,看不懂里面的正则表达式??

385 阅读2分钟

学习背景

最近想做一个工具:可以将项目中的 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",它都可以提取出来。

分解其结构:

  1. ^\s*: 这个就比较简单,^在这里表示匹配开头, \s表示空白符(space character的首字母),*表示出现任意次,连起来就是从0到无穷多的空白符
  2. ([^\s"'<>\/=]+): 最后的+代表至少一次,然后[^\s"'<>\/=]表示除了\s,", ',<,>,/,=之外所有的字符, 连起来就是出现至少一次除了刚刚那几个之外的字符,这里其实就是匹配的属性名,比如 id,class等等
  3. (?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?
    • ?:表示括号内的是非捕获分组,可以不用看
    • \s*(=)\s*: 主要就是匹配个=符号
    • ?:"([^"]*)"+: 匹配双引号里面的内容
      • ?: 表示非捕获分组
      • ": 匹配"符号
      • ([^"]*): 除了"符号之外的任意字符,出现任意次数
      • "+: 匹配"符号,至少一次
    • '([^']*)'+: 匹配单引号里面的内容
      • ': 匹配'符号
      • ([^']*): 除了'符号之外的任意字符,出现任意次数
      • '+: 匹配'符号,至少一次
    • ([^\s"'=<>`]+): 除了空白符号,",',=,<,>之外的字符,出现至少一次
    • ?: 三个分组中匹配 0 次或 1 次

WeChat41b2ad1142b7675c551f9ae466bbfd07.png

dynamicArgAttribute

const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

dynamicArgAttribute 主要匹配的事动态属性,例如

<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>

分解其结构:

  1. ^\s*: 如上, 空白字符开头
  2. ((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*): 匹配=号前面的内容
    • v-[\w-]+:|@|:|#:
      • v-: 以v-开头
      • [\w-]+: \w表示包括下划线在内的单个字符,[\w-]+表示\w以及-匹配至少一次
      • :|@|:|#: :@#
    • \[[^=]+?\][^\s"'<>\/=]*
      • \[[^=]+?\]: \[转译符匹配[[^=]+?表示除了=之外的字符至少一次并且+?表示尽量少的重复,\]则匹配]
      • [^\s"'<>\/=]*: 除了空白符号,",',<,>, =, /之外的字符,出现至少一次
  3. (?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?: 跟上面attritube一致, 不再重复

WeChat2275deabd33396b778de161e0f378e19.png

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}[^>]*>`)
  1. ^<\\/: 表示以</为开头
  2. [^>]*: 表示除了>的字符,任意数量

这个比较简单,就是匹配结束标签, 例如</div>

doctype

const doctype = /^<!DOCTYPE [^>]+>/i
  1. /^<!DOCTYPE: 表示以<!DOCTYPE开头
  2. [^>]+: 表示除了>的字符,出现至少一次

匹配如: <!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]-->

参考文献