对来自 Vue 源码的一段复杂正则的分析

说明

今天在看 Vue 源码中的解析SFC(Single File Component)部分中的解析 html 部分时看到一串很长的正则表达式。具体位置在 /src/compiler/parser/html-parser.js:16html

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
复制代码

主要使用在 /src/compiler/parser/html-parser.js:189-209vue

function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }
复制代码

这段代码的目的是从一段 html 字符串中把一个开始标签匹配出来,而后把开始标签内的全部属性再匹配出来,放到一个数组内。相信不只仅是我,让你们在短期内写出这样的一个正则表达式都是比较困难的。那么我就今天就来详细的去分析一下这个复杂的正则表达式是如何实现的,以及它能匹配到什么和不能匹配到什么。其中顺便会介绍一些正则的基础内容,高手勿喷。文章略长,Be Patient.git

分而治之

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
复制代码

初看这段正则表达式,很长,对正则不熟悉的人可能会被吓一跳,甚至直接跳过去不看。这里给你们介绍的一个方法就是“分而治之”:就是把一个很长的正则表达式分割成一个个的短的表达式,分别去理解。如上表达式,咱们能够初步分割成以下:github

/^\s*   ([^\s"'<>\/=]+) (?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ (1) (2) (3) 复制代码

第一部分

先来看标注的第 (1) 部分,也是最简单的部分:^ 表示匹配输入的开始,\s表示空格,* 表示可无可有可多个。那么整个第一部分的意思就很清楚了:输入的字符的开头可无、可有、可多个空格。若是单独这块儿做为一个表达式来匹配的话:正则表达式

const part1 = /^\s*/;
'abc'.match(part1); // 匹配到空字符串
' abc'.match(part1); // 匹配到一个空格
' abc'.match(part1); // 匹配到两个空格
复制代码

第二部分

接下来看第二部分:([^\s"'<>\/=]+):首先,第二部分被一个 () 包裹着。在正则里面这叫作捕获分组。什么意思呢?“捕获”和“分组”,就是说会把这部分匹配到的结果看成一个分组捕获出来。捕获出来就是在知足整个大的正则表达式的基础上,会将知足这个分组表达式的字符串看成一个小的分组结果放进大的结果数组中。好比:数组

const group = /a(.*)a/;
`a1232a`.match(group); // => ['a1232a', '1232'];
// 结果[0]是知足整个表达式的匹配结果,结果[1]是在大结果中的一个知足()内表达式的一个小的结果分组
复制代码

看明白上面以后,咱们执行大脑出栈,从 () 的研究中跳回来再来看第二部分的表达式。bash

() 以内是紧接着的一个 [] 部分和一个 +[]表示里面的内容是一个字符集合,主要就是对字符进行限制。在 [] 内的第一个字符就出现了 ^ 字符。这里的 ^ 字符和刚才出现的 ^ 字符彻底不同,由于这里是出如今字符集合的第一个字符,表示的是 “非” 的意思,就是不能出现字符集合中的字符。再来看看有哪些字符不能出现呢?分别是: \s,",',<,>,\/,=(空格,双引号,单引号,小于号,大于号,右斜线)。这些不能出现,也就是说除了这些其余字符均可以。再来看后面的 +,方才说 * 是“可无可有可多个”,那么 + 就是 “可有可多个”(至少一个)。spa

至此,咱们知道这段表达式是要匹配哪些东西呢?除了空格,双引号,单引号,小于号,大于号,右斜线这些字符外的字符组成的字符串!好比:设计

const part2 = /([^\s"'<>\/=]+)/;
'name'.match(part2); // => ['name', 'name'];
' name'.match(part2); // => ['name', 'name']; 这个为何能匹配到?由于没有在正则表达式的前面加 '^'限制。
复制代码

那么咱们把前面两部分合起来看:code

const part1_2 = /^\s*([^\s"'<>\/=]+)/;
'name="benchen"'.match(part1_2); // => ["name", "name", index: 0, input: "name="benchen""]
' +="benchen"'.match(part1_2); // => [" +", "+", index: 0, input: " +="benchen""]
' ="benchen"'.match(part1_2); // => null 
// 为何呢?第一个空格知足了第一部门的匹配,可是在空格以后紧跟着的是一个等号
// 在第二部分的匹配中禁止出现'='字符,因此匹配不到结果。
复制代码

第三部分(坚持啊)

接下来看,看上去很复杂的第三部分。

(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>]+)))?`

第三部分的最后面有个 ?,刚才介绍了 *+? 表示的是“可无可有”。咱们先来稍做总结吧:(这里的有表示有一个)

  • ?: 可无可有 (没有或一个)
  • +: 可有可多个 (至少一个)
  • *: 可无可有可多个 (随便几个)

那么再回来,也就是说第三部分这个分组的匹配,能够知足,也能够不知足。

咱们再使用分而治之的方法对第三组进行分解:

(?:  \s*(=)\s*  (?:  "([^"]*)"+ | '([^']*)'+ | ([^\s"'=<>`]+ )))? (1) (2) (3) (4) 复制代码

第一部分:无关紧要可多个的空格后面跟着一个必须的等号,等候后面可无可有可多个空格。

第二部分:双引号之间有随便多少个由非双引号构成的字符串。因此"abc"能够, """不能够。

第三部分:和第二部分相似,把双引号换成单引号

第四部分:非 空格、双引号、单引号、等号、小于号、大于号、反单引号(`) 组成的非空字符串。

注意:二、三、4部分是或的关系,只要知足任何一个就能够。

整合

终于到了整合到一块儿看的时候了,看看这个过滤网能过滤出哪些东西。

/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
复制代码

语言描述:输入字符串的开头能够没有,也能够有随便多个空格,紧跟着的是一个字符串,这个字符串的字符组成必须不含有空格、双引号、单引号、等号、小于号、大于号、反单引号(`),后面能够有也能够没有第三个分组。若是有第三个分组必须知足这样的逻辑:无关紧要的空格后面跟着一个等号,后面可又可无空格,再后面能够是双引号包裹的个字符串,其中不能含有双引号;能够是单引号包裹的字符换,其中不能有单引号,能够是非 空格、双引号、单引号、等号、小于号、大于号、反单引号(`) 组成的非空字符串。

算了,好复杂,我放弃了,我认可人类的语言远远没有正则表达式更具备表现力。那咱们就来看例子吧:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

// 在以前咱们先看一下一共有 5 个捕获分组,因此匹配结果数组应该有 6 个值。
// 为了方便看,在后面的结果中我省略了 index, input, length 等属性。

'name="benchen"'.match(attribute); // 最简单的
//=> ["name="benchen"", "name", "=", "benchen", undefined, undefined]
' name="benchen"'.match(attribute); // 前面有空格
//=> [" name="benchen"", "name", "=", "benchen", undefined, undefined]
' name = "benchen"'.match(attribute); // 等号先后有空格
//=> [" name = "benchen"", "name", "=", "benchen", undefined, undefined]
` name = 'haha'`.match(attribute); // 值被单引号包裹
//=> [" name = 'haha'", "name", "=", undefined, "haha", undefined]
` name = haha`.match(attribute); // 值不被包裹
//=> [" name = haha", "name", "=", undefined, undefined, "haha",]
'name'.match(attribute); // 只有属性名没有值
//=> ["name", "name", undefined, undefined, undefined, undefined]
'+=+'.match(attribute); // 搞个变态的
//=> ["+=+", "+", "=", undefined, undefined, "+"]
'@click="clickHandler"'.match(attribute); // vue 的事件绑定
//=> ["@click="clickHandler"", "@click", "=", "clickHandler", undefined, undefined]
':name="name"'.match(attribute); // 数据传递
//=> [":name="name"", ":name", "=", "name", undefined, undefined]
'v-model="model"'.match(attribute); // 数据传递
//=>  ["v-model="model"", "v-model", "=", "model", undefined, undefined]
复制代码

匹配不到结果的输入

'="benchen"'.match(attribute) // null,开始的'='不符合第二部分匹配,
复制代码

不该该被匹配到的输入

'name=="benchen"'.match(attribute);
//=> ["name", "name", undefined, undefined, undefined, undefined]
// 在我看来上面的输入不该该匹配出结果,这多是这个正则不完美的地方吧,算不上漏洞。
复制代码

总结

其实不论是多么复杂的正则表达式都是有好多个分组组成的,在分析或着设计的时候能够一组一组的来,下降理解的复杂度。

🔗原文连接

相关文章
相关标签/搜索