exp-parser | 每天讀一點Vue原始碼

VimMing發表於2020-04-04

今天是2020年4月4日清明節,向英雄致敬,向逝者致哀...

前言

面試的時候經常被問一些Vue原始碼相關的問題,通常情況下, 我會在面試前惡補掘金上的麵筋來對付面試,什麼雙向繫結的原理呀,什麼虛擬dom樹呀,實際上我壓根兒就沒仔細研究過,其一是自己真的比較菜,其二工作上也用不上,別自己給自己添堵。但後面想一下,很多事情,為之則易,不為則難,給自己設立困難(負重)才能進步,決定每天多一點Vue的原始碼,在Vue的原始碼選擇上,我選擇了最老的版本(0.1)?(真的怕自己看起來吃力), 閱讀的模式為通讀,從易到難一個檔案一個檔案的看,看完一個檔案後再看它的單元測試,等完全吃透後複製貼上程式碼到本地執行測試用例為程式碼塊寫一些中文註釋,打上tag推到自己的倉庫,開始梳理寫文章總結(之前有猶豫過是否應該在掘金上寫文章,因為這類Vue原始碼解析的文章已經很多了,而且還寫的很好,我再寫一遍是否還存在意義,後面想還是寫吧,流水總結也不錯?)。

正文

這是我發的第五篇關於Vue原始碼的文章, 本文介紹exp-parser(表示式解析), exp-parser把表示式比如 a + b轉換為可以執行的匿名函式() => {return this.a + this.b;}看下面詳細的例子:

<span>{{a - b * 2 + 45}}</span> 
<span>{{(a && b) ? c : d || e}}</span>
<span>{{todo.title + ' : ' + (todo.done ? 'yep' : 'nope')}}</span>
<span>{{sortRows({ column: 'name', test: 'haha', durrr: 123 })}}</span>
複製程式碼
// 上面有4個表示式,
// 4個表示式經過exp-parser將轉換成大致這樣:
// a - b * 2 + 45
() => {
    return this.a - this.b * 2 + 45
}
// (a && b) ? c : d || e
() => {
    return (this.a && this.b) ? this.c : this.d || this.e
}
// todo.title + ' : ' + (todo.done ? 'yep' : 'nope')
() => {
    return this.todo.title + ' : ' + (this.todo.done ? 'yep' : 'nope')
}
// sortRows({ column: 'name', test: 'haha', durrr: 123 })
() => {
    return this.sortRows({column: 'name', test: 'haha', durre: 123})
}
複製程式碼

從上面的例子中可以大致得知exp-parser的實現分成三步, 第一步獲得表示式裡面的變數名 比如上面的a, b, c, todo, sortRows, 第二部根據變數名找到其所在的作用域this.a 或者 this.$parent.a,第三步替換上述的變數,返回匿名函式() => {return this.a}

// 下面是虛擬碼
exports.parse = function (exp, compiler, data) {
    // 獲得變數名
    var vars = getVariables(exp)
    // 獲得變數的作用域
    vars.forEach((path, i) => {
      vars[i] = traceScope(path, compiler, data)  
    })
    // 根據exp,和vars生成匿名函式
    return makeGetter(vars, exp)
}

複製程式碼

上面是exp-parser原始碼的大致雛型,exp-parser大致160行左右,整體閱讀不難,而我在閱讀原始碼的時候,遇到的最大問題是exp-parser的正規表示式是真的多,看到腦殼痛,時間也大部分花在閱讀正規表示式上面去了,為了掃平會遇到的閱讀障礙,下面重點講解getVariables函式裡的正規表示式。

/**
 *  Strip top level variable names from a snippet of JS expression
 */
// 提取表示式中的變數
// getVariables("(a && b) ? c : d || e") => [a, b, c, d, e]
function getVariables(code) {
    code = code.replace(REMOVE_RE, '')
        .replace(SPLIT_RE, ',')
        .replace(KEYWORDS_RE, '')
        .replace(NUMBER_RE, '')
        .replace(BOUNDARY_RE, '')
    return code ? code.split(/,+/) : []
}

複製程式碼

getVariables函式用了5個正規表示式才把表示式中的變數提取出來,下面講解下REMOVE_RE, KEYWORDS_RE這兩個正規表示式,這2個有點小複雜,同時也能學到一些東西。

REMOVE_RE

用於刪除內容的正則

var REMOVE_RE = /\/\*(?:.|\n)*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|'[^']*'|"[^"]*"|[\s\t\n]*\.[\s\t\n]*[$\w\.]+|[\{,]\s*[\w\$_]+\s*:/g
複製程式碼

這段正則巨長,其實可以分割成一下幾段正則:

var REMOVE_RE1 =  /\/\*(?:.|\n)*?\*\//
REMOVE_RE1.test("/*xxxxxxx*/") // true
var REMOVE_RE2 = /\/\/[^\n]*\n/
REMOVE_RE2.test("//xxxxxx\n") // true
var REMOVE_RE3 = /\/\/[^\n]*$/
REMOVE_RE3.test("//xxxxxx") // true
var REMOVE_RE4 = /'[^']*'/
REMOVE_RE4.test("'xxxxxx'") // true
var REMOVE_RE5 = /"[^"]*"/
REMOVE_RE5.test('"xxxxxxx"') // true
var REMOVE_RE6 = /[\s\t\n]*\.[\s\t\n]*[$\w\.]+/
REMOVE_RE6.test(".xxxx") // true
var REMOVE_RE7 = /[\{,]\s*[\w\$_]+\s*:/
REMOVE_RE7.test("{ a: 1, b: ")
複製程式碼

上述正則能匹配的內容為test函式裡面的字串,我順便解釋下這些正則裡面的元字元: (?:)非捕獲組, \n匹配換行, \s匹配空格, \t匹配製表符即鍵盤上的tab鍵, \w匹配字元或數字。

KEYWORDS_RE

匹配js裡面的關鍵字

// 看程式碼的那天正好得知司徒正美大佬去了只有二次元的世界 R.I.P。
// Variable extraction scooped from https://github.com/RubyLouvre/avalon

// javascript中的關鍵字
var KEYWORDS =
    // keywords
    'break, case, catch,continue,debugger,default,delete,do,else,false' +
    ',finally,for,function,if,in,instanceof,new,null,return,switch,this' +
    ',throw,true,try,typeof,var,void,while,with,undefined' +
    // reserved
    ',abstract,boolean,byte,char,class,const,double,enum,export,extends' +
    ',package,private,protected,public,short,static,super,synchronized' +
    'throws,transient,volatile' +
    // ECMA 5 - use strict
    ',arguments,let,yield' +
    //allow using Math in expressions
    ',Math'
    
    // 匹配關鍵字
var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b')].join('|')),
複製程式碼

Vue-0.1中keywords來源於avalon,而我看程式碼的那天順便在刷掘金沸點,刷到了avalon的作者去世了,這也是緣分呀,願大佬R.I.P。

這段正規表示式不難,我當時看的時候,唯一疑惑的是\b, 因為我分不清\b\B的區別, 我相信應該有很多人像我一樣分不清,我查資料獲得\b非字母和數字字元和數字的分隔符, \B字元的分隔符,是不是聽起來繞,看下面一段程式碼: 我們用 |代表\b, 用-代表\B

var tt = "abc"
tt.replace(/\b/g, '|').replace(/\B/g, '-')
// 結果: tt: "-|a-b-c|-"
複製程式碼

最後

如果實在是被exp-parser.js裡面的正則搞暈了,建議看下這篇文章,幫你複習一下正規表示式javascript正規表示式 | 知識梳理?。

exp-parser.js

exp-parser.test.js

持續更新...❤️

相關文章