Vue0.11版本源碼閱讀系列四:詳解指令值解析函數(shù)
需求
首先該版本的vue指令值支持一下幾種類型以及通過dirParser.parse要返回的數(shù)據(jù):
1.實例屬性:message,解析后應(yīng)為:
[ { "raw":"message", "expression":"message" } ]2.表達(dá)式:message === show,解析后應(yīng)為:
[ { "raw":"message === show", "expression":"message === show" } ]3.三元表達(dá)式:show ? true : false,解析后應(yīng)為:
[ { "raw":"message === show ? true : false", "expression":"message === show ? true : false" } ]4.設(shè)置元素類名、樣式、屬性、事件的:red:hasError,bold:isImportant,hidden:isHidden,解析后應(yīng)為:
[ {"arg":"red","raw":"red: hasError","expression":"hasError"}, {"arg":"bold","raw":"bold: isImportant","expression":"isImportant"}, {"arg":"hidden","raw":"hidden: isHidden","expression":"isHidden"} ]或者:top: top + px,left: left + px,background-color: rgb(0,0, + bg + ),解析后應(yīng)為:
[ {"arg":"top","raw":"top: top + px","expression":"top + px"}, {"arg":"left","raw":"left: left + px","expression":"left + px"}, {"arg":"background-color","raw":"background-color: rgb(0,0, + bg + )","expression":"rgb(0,0, + bg + )"} ]5.雙大括號插值的:{{partialId}},解析后應(yīng)為:
[ { "raw":"{{partialId}}", "expression":"{{partialId}}" } ]6.指令值是數(shù)組或?qū)ο?/p>
[1, 2, 3]應(yīng)解析為:
[ { "raw":"[1, 2, 3]", "expression":"[1, 2, 3]" } ]{arr: [1, 2, 3], value: abc}應(yīng)解析為:
[ { "raw":"{arr: [1, 2, 3], value: abc}", "expression":"{arr: [1, 2, 3], value: abc}" } ]7.過濾器:
message | capitalize應(yīng)解析為:
[ { "expression":"message", "raw":"message | capitalize", "filters":[ { "name":"capitalize", "args":null } ] } ]帶參數(shù)的message | capitalize 4 5應(yīng)解析為:
[ { "expression":"message", "raw":"message | capitalize 4 5", "filters":[ { "name":"capitalize", "args":["4","5"] } ] } ]多個過濾器之間使用|進行分隔。
總結(jié)一下,就是如果是以逗號分隔的冒號表達(dá)式,則解析為:
[ { arg: 【冒號前的字符】, expression: 【冒號后的字符】, raw: 【原始值】 }, ... ]帶過濾器的會多一個filters字段。
其他一律解析為:
[ { expression: 【和原始值一樣的值】, raw: 【原始值】 } ]實現(xiàn)
現(xiàn)在讓我們從0開始寫一個解析器:
let dirs = [] function parse(s) { return dirs }簡單的變量
首先支持最簡單的第一種,實例屬性或方法,根據(jù)上面的對照,基本原封不動返回即可:
var str = var dirs = [] var dir = {} var begin = 0 var i = 0 exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) {} pushDir() return dirs } function pushDir () { dir.raw = str.slice(begin, i) dir.expression = str.slice(begin, i) dirs.push(dir) }可以看到完全就是為了得到目標(biāo)值的一個多此一舉的過程,下一步來支持逗號分隔的冒號表達(dá)式。
冒號表達(dá)式
先看就一個的情況,如a:b,遍歷到的當(dāng)前字符如果是冒號的話就把冒號之前的字符截取出來作為arg,冒號后的字符作為expression,begin變量是用來標(biāo)記當(dāng)前這個表達(dá)式的起點的,所以要截取冒號后的字符需要新增一個變量:
var str = var dirs = [] var dir = {} var begin = 0 var argIndex = 0 // ++ var i = 0 exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { // ++ c = str.charCodeAt(i) if (c === 0x3A) {// 冒號: dir.arg = str.slice(begin, i).trim() argIndex = i + 1 } } pushDir() return dirs } function pushDir () { dir.raw = str.slice(begin, i) dir.expression = str.slice(argIndex, i).trim()// ++ dirs.push(dir) }接下來支持存在逗號的情況,逗號相當(dāng)于一個表達(dá)式的結(jié)束,所以要進行一次push,另外begin、argIndex、dir變量都需要重置:
exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (c === 0x3A) {// 冒號: dir.arg = str.slice(begin, i).trim() argIndex = i + 1 } else if (c === 0x2C) {// 逗號, ++ pushDir() dir = {} begin = argIndex = i + 1 } } pushDir() return dirs }三元表達(dá)式
接下來支持三元表達(dá)式,目前會把三元表達(dá)式的冒號前后部分分離調(diào),會輸出類似下面的結(jié)果:
[ { "arg":"show ? true", "raw":"show ? true : false", "expression":"false" } ]所以檢查到冒號的時候我們要判斷一下這是否是個三元表達(dá)式,是的話就不截?。?/p>exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (c === 0x3A) {// 冒號: ++ var arg = str.slice(begin, i).trim() if (/^[^\?]+$/.test(arg)) { dir.arg = arg argIndex = i + 1 } } else if (c === 0x2C) { pushDir() dir = {} begin = argIndex = i + 1 } } pushDir() return dirs }
判斷一下冒號之前的字符里是否存在?,存在的話就代表是三元表達(dá)式,則不進行分割。
看一個特殊情況:background-color: rgb(0,0, + bg + )
目前會解析成:
[ { "arg":"background-color", "raw":"background-color: rgb(0", "expression":"rgb(0" }, { "raw":"0", "expression":"0" }, { "raw":" + bg + )", "expression":" + bg + )" } ]原因就出在屬性值里的逗號,如果屬性值里存在逗號,那該屬性值一定是被引號包圍的,所以在單引號或雙引號里的都要忽略,所以讓我們新增兩個變量來記錄是否是在引號里:
var inDouble = false // ++ var inSingle = false // ++如果出現(xiàn)第一個引號,把標(biāo)志設(shè)為true,然后中間字符都直接跳過,直到出現(xiàn)閉合的引號,才退出繼續(xù)其他的判斷:
exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (inDouble) {// 雙引號還未閉合 ++ if (c === 0x22) {// 出現(xiàn)了閉合引號 inDouble = !inDouble } } else if (inSingle) {// 單引號還未閉合 ++ if (c === 0x27) {// 出現(xiàn)了閉合引號 inSingle = !inSingle } } else if (c === 0x3A) { // ... } else if (c === 0x2C) { // ... } else {// ++ switch (c) { // 首次出現(xiàn)引號設(shè)置標(biāo)志位 case 0x22: inDouble = true; break // " case 0x27: inSingle = true; break // default: break; } } } pushDir() return dirs }數(shù)組或?qū)ο?/h2>
數(shù)組或?qū)ο蠖夹枰獠粍拥姆祷?,因為帶冒號和逗號目前都會被切割,對?shù)組來說,字符都是被[]中括號包圍的,所以在這區(qū)間的逗號要忽略掉,因為括號可能多重嵌套,所以增加一個變量來計數(shù),出現(xiàn)左括號加1,出現(xiàn)右括號減1,為0就代表不在括號里:
var square = 0// ++ exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (inDouble) {} else if (inSingle) {} else if (c === 0x3A) {} else if (c === 0x2C && square === 0) {// ++ pushDir() dir = {} begin = argIndex = i + 1 } else { switch (c) { case 0x22: inDouble = true; break case 0x27: inSingle = true; break case 0x5B: square++; break // [ ++ case 0x5D: square--; break // ] ++ default: break; } } } pushDir() return dirs }對象也是類似,但是對象多了冒號,冒號也會被截掉,所以需要和三元表達(dá)式一樣進行判斷:
var curly = 0// ++ exports.parse = function (s) { str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (inDouble) {} else if (inSingle) {} else if (c === 0x3A) { var arg = str.slice(begin, i).trim() if (/^[^\?\{]+$/.test(arg)) {// ++ 正則表達(dá)式修改,如果出現(xiàn)了{(lán)代表可能是對象 dir.arg = arg argIndex = i + 1 } } else if (c === 0x2C && square === 0 && curly === 0) {// ++ pushDir() dir = {} begin = argIndex = i + 1 } else { switch (c) { // ... case 0x7B: curly++; break // { ++ case 0x7D: curly--; break // } ++ default: break; } } } pushDir() return dirs }過濾器
最后來看過濾器,過濾器使用管道符|,所以遍歷到這個字符時推入過濾器,過濾器支持多個,第一個字符串代表表達(dá)式,后續(xù)|分隔的各代表一個過濾器,當(dāng)出現(xiàn)第一個|時只能獲取到該過濾器所被應(yīng)用的值,也就是expression的值,需要繼續(xù)遍歷才知道具體的過濾器,如何判斷是否是第一個|可以根據(jù)expression是否有值:
exports.parse = function (s) { for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) // ... else if (c === 0x7C) {// 管道符| if (dir.expression === undefined) {// 第一次出現(xiàn)| dir.expression = str.slice(argIndex, i).trim()// 截取第一個|前的字符來作為表達(dá)式的值 } } // ... } } function pushDir () { dir.raw = str.slice(begin, i) if (dir.expression === undefined) {// ++ 這里也需要進行判斷,如果有值代表已經(jīng)被過濾器分支設(shè)置過了,這里就不需要設(shè)置 dir.expression = str.slice(argIndex, i).trim() } dirs.push(dir) }假設(shè)只有一個過濾器的話繼續(xù)遍歷會直到結(jié)束,結(jié)束后會再調(diào)一次pushDir方法,所以修改一下這個方法,進行一次過濾器收集處理:
function pushDir () { dir.raw = str.slice(begin, i) if (dir.expression === undefined) { dir.expression = str.slice(argIndex, i).trim() } else {// ++ 添加過濾器 pushFilter() } dirs.push(dir) } function pushFilter () { // 這里要截取的字符串應(yīng)該是|后面的,begin和argIndex字段都用不了,所以需要新增一個變量 }新增一個變量用于記錄當(dāng)前過濾器的起始位置:
var lastFilterIndex = 0 // ++ exports.parse = function (s) { for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) // ... else if (c === 0x7C) { if (dir.expression === undefined) { dir.expression = str.slice(argIndex, i).trim() } lastFilterIndex = i + 1// ++ } // ... } }因為過濾器支持帶參數(shù),參數(shù)和過濾器名之間用空格分隔,所以寫一個正則來匹配一下:/[^\s"]+|[^]+|"[^"]+"/g,參數(shù)除了是變量也可以是字符串,所以后面兩個對引號的匹配是為了保證最后匹配的結(jié)果也是帶引號的,否則:capitalize abc和capitalize abc最后匹配出來的都是:["abc"],加上后面兩個引號的匹配后則才是我們需要的:["abc"]。
function pushFilter() { var exp = str.slice(lastFilterIndex, i).trim() if (exp) { var tokens = exp.match(/[^\s"]+|[^]+|"[^"]+"/g) var filter = {} filter.name = tokens[0] filter.args = tokens.length > 1 ? tokens.slice(1) : null dir.filters = dir.filters || [] dir.filters.push(filter) } }結(jié)果如下:
接下來支持一下多個過濾器的情況,多個過濾器,則會出現(xiàn)多個|,所以又會走到|的if分支,非第一次出現(xiàn)的話不需要修改expression的值,直接push當(dāng)前遍歷到的過濾器即可:
exports.parse = function (s) { for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) // ... else if (c === 0x7C) { if (dir.expression === undefined) { dir.expression = str.slice(argIndex, i).trim() } else {// 非第一次出現(xiàn)直接push ++ pushFilter() } lastFilterIndex = i + 1 } // ... } }結(jié)果如下:
最后也看一種特殊情況,就是||的情況,這是或的意思,所以肯定不能解析為過濾器,在if條件里增加一下判斷,排除當(dāng)前遍歷到的|前一個或后一個字符也是|的情況:
exports.parse = function (s) { for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) // ... else if (c === 0x7C && str.charCodeAt(i - 1) !== 0x7C && str.charCodeAt(i + 1) !== 0x7C) {// ++ // ... } // ... } }完成
到這里基本就完成了,完整代碼如下:
var str = var dirs = [] var dir = {} var begin = 0 var argIndex = 0 var i = 0 var inDouble = false var inSingle = false var square = 0 var curly = 0 var lastFilterIndex = 0 function reset() { str = dirs = [] dir = {} begin = 0 argIndex = 0 i = 0 inDouble = false inSingle = false square = 0 curly = 0 lastFilterIndex = 0 } exports.parse = function (s) { reset() str = s for (i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i) if (inDouble) {// 雙引號還未閉合 if (c === 0x22) {// 出現(xiàn)了閉合引號 inDouble = !inDouble } } else if (inSingle) {// 單引號還未閉合 if (c === 0x27) {// 出現(xiàn)了閉合引號 inSingle = !inSingle } } else if (c === 0x3A) {// 冒號: var arg = str.slice(begin, i).trim() if (/^[^\?\{]+$/.test(arg)) { dir.arg = arg argIndex = i + 1 } } else if (c === 0x2C && square === 0 && curly === 0) {// 逗號, pushDir() dir = {} begin = argIndex = i + 1 } else if (c === 0x7C && str.charCodeAt(i - 1) !== 0x7C && str.charCodeAt(i + 1) !== 0x7C) {// 管道符| if (dir.expression === undefined) {// 第一次出現(xiàn)| dir.expression = str.slice(argIndex, i).trim() } else {// 非第一次出現(xiàn)直接push pushFilter() } lastFilterIndex = i + 1 } else { switch (c) { case 0x22: inDouble = true; break // " case 0x27: inSingle = true; break // case 0x5B: square++; break // [ case 0x5D: square--; break // ] case 0x7B: curly++; break // { case 0x7D: curly--; break // } default: break; } } } pushDir() return dirs } function pushDir () { dir.raw = str.slice(begin, i) if (dir.expression === undefined) {// ++ 這里也需要進行判斷,如果有值代表已經(jīng)被過濾器分支設(shè)置過了,這里就不需要設(shè)置 dir.expression = str.slice(argIndex, i).trim() } else {// ++ 添加過濾器 pushFilter() } dirs.push(dir) } function pushFilter() { var exp = str.slice(lastFilterIndex, i).trim() if (exp) { var tokens = exp.match(/[^\s"]+|[^]+|"[^"]+"/g) var filter = {} filter.name = tokens[0] filter.args = tokens.length > 1 ? tokens.slice(1) : null dir.filters = dir.filters || [] dir.filters.push(filter) } }把上面的代碼替換掉vue源碼里的相關(guān)代碼,測試了一下基本用例是能跑通的,但是可能還會有其他一些特殊場景沒有照顧到,更完善的代碼請自行閱讀vue源碼。
掃描二維碼推送至手機訪問。
版權(quán)聲明:本文由財神資訊-領(lǐng)先的體育資訊互動媒體轉(zhuǎn)載發(fā)布,如需刪除請聯(lián)系。