今天在看 Vue 源碼中的解析SFC(Single File Component)部分中的解析 html 部分時看到一串很長的正則表達式。具體位置在 /src/compiler/parser/html-parser.js:16
html
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
複製代碼
主要使用在 /src/compiler/parser/html-parser.js:189-209
:vue
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]
// 在我看來上面的輸入不該該匹配出結果,這多是這個正則不完美的地方吧,算不上漏洞。
複製代碼
其實不論是多麼複雜的正則表達式都是有好多個分組組成的,在分析或着設計的時候能夠一組一組的來,下降理解的複雜度。