阅读 760

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

说明

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

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

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

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.

分而治之

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

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

/^\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]是在大结果中的一个满足()内表达式的一个小的结果分组
复制代码

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

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

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

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

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

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"可以, """不可以。

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

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

注意:2、3、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]
// 在我看来上面的输入不应该匹配出结果,这可能是这个正则不完美的地方吧,算不上漏洞。
复制代码

总结

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

🔗原文链接