博客源地址: https://github.com/LeuisKen/l...
相關評論還請到 issue 下。
san.parseExpr是San中主模塊下的一個方法。用於將源字符串解析成表達式對象。該方法和san.evalExpr是一對,後者接收一個表達式對象和一個san.Data對象做爲參數,用於對錶達式進行求值。以下例:html
/** * 解析表達式 * * @param {string} source 源碼 * @return {Object} 表達式對象 */ function parseExpr(source) {} /** * 計算表達式的值 * * @param {Object} expr 表達式對象 * @param {Data} data 數據容器對象 * @param {Component=} owner 所屬組件環境,供 filter 使用 * @return {*} */ function evalExpr(expr, data, owner) {} san.evalExpr(san.parseExpr('1+1'), new san.Data()); // 2 san.evalExpr(san.parseExpr('1+num'), new san.Data({ num: 3 })); // 4
單獨拿出parseExpr
來分析,其根據源字符串生成表達式對象,從San的表達式對象文檔中,能夠看到San支持的表達式類型以及這些表達式對象的結構。咱們在這裏簡單記錄一下,parseExpr
須要解析的表達式都有哪些:node
除了上述表示運算關係的表達式外,還有表示數據的表達式,以下:git
因爲Accessor
存在乎義,是爲了在evalExpr
階段從Data
對象中獲取數據,因此這裏我將Accessor
歸類爲表示數據的表達式。github
如今咱們知道了全部的表達式類型,那麼,parseExpr
是如何從字符串中,解析出表達式對象的呢?正則表達式
parseExpr
方法定義在src/parser/parse-expr.js中。咱們能夠看到其依賴了一個Walker類,註釋中的說明是字符串源碼讀取類。數組
Walker
類包含如下內容:ide
this.source
:保存要讀取的源字符串this.len
:保存源字符串長度this.index
:保存當前對象讀取字符的位置currentCode
方法:返回當前讀取字符的 charCodecharCode
方法:返回指定位置字符的 charCodecut
方法:根據指定起始和結束位置返回字符串片斷go
方法:將this.index
增長給定數值nextCode
方法:讀取下一個字符並返回它的 charCode/** * 向前讀取字符,直到遇到指定字符再中止 * 未指定字符時,當遇到第一個非空格、製表符的字符中止 * * @param {number=} charCode 指定字符的code * @return {boolean} 當指定字符時,返回是否碰到指定的字符 */ Walker.prototype.goUntil = function (charCode) { var code; while (this.index < this.len && (code = this.currentCode())) { switch (code) { // 空格 space case 32: // 製表符 tab case 9: this.index++; break; default: if (code === charCode) { // 找到了 this.index++; return 1; } // 沒找到 return; } } };
/** * 向前讀取符合規則的字符片斷,並返回規則匹配結果 * * @param {RegExp} reg 字符片斷的正則表達式 * @param {boolean} isMatchStart 是否必須匹配當前位置 * @return {Array?} */ Walker.prototype.match = function (reg, isMatchStart) { reg.lastIndex = this.index; var match = reg.exec(this.source); /** * 這裏是源碼的實現,簡潔可是有點晦澀,後面我把邏輯運算符拆成了 if else,但願能好理解一些 if (match && (!isMatchStart || this.index === match.index)) { this.index = reg.lastIndex; return match; } */ if (match) { // 若是是必須匹配當前位置 // 這個標記是 3.5.11 的時候加上的,changelog 表述爲: // 【優化】- 在 dev 模式下,增長一些表達式解析錯誤的提示 if (isMatchStart) { // 判斷當前讀取字符的 index,是否和匹配結果第一個字符的 index 相等 if (this.index === match.index) { this.index = reg.lastIndex; return match; } } // 沒必要須匹配當前位置 else { this.index = reg.lastIndex; return match; } } };
在初看parseExpr
實現的時候,這就是一個困擾個人難題。學習過程當中,我看到San
最早是將表達式丟給一個讀取三元表達式的方法,這個方法裏面去調用讀取邏輯或表達式的方法,邏輯或裏面調用邏輯與,邏輯與裏面調用判等,判等裏面調用關係⋯⋯看得我能夠說是雲裏霧裏。雖然大體能明白這是在處理運算優先級,可是我以爲確定有一個更上層的指導思想來讓San
選擇這一方案。函數
爲了尋找這個「指導思想」,我轉頭去看了一段時間的編譯原理,大體上理清了這部分思路。考慮到有些同窗應該也和我同樣沒有系統地學習過這門課程,所以我在下面取《編譯原理》中的例子來予以說明(下文內容包含了不少定義性的內容,且爲了保證嚴謹,不少定義都是直接照搬書上的,因此若是你對這部分足夠熟悉,跳過便可。)學習
假設咱們如今要解析的expr
是一個十之內的四則運算算式(編譯原理將其視爲一種語言),其包括加減乘除( +、-、*、/ )四則運算。咱們可使用一種叫作產生式的方式,來表示表達式的解析規則。有了產生式,咱們能夠將一個算式的解析規則表達成以下形式(這一解析過程被稱爲詞法分析):優化
expr ---> digit // 這裏的 digit 指 0,1,2,3...9 這十個數字 | expr + expr // 豎線(|)表示或,這一行定義了加法 | expr - expr // 減法 | expr * expr // 乘法 | expr / expr // 除法 | (expr) // 加括號
這裏介紹幾個概念,這裏的digit
和+ - * / ()
等符號,被稱爲終結符號,表示語言中不可再分的基本符號;而像expr
這樣可以用於表示終結符號序列的變量,被稱爲非終結符號。
咱們都知道,十之內的四則運算算式的解析是與上下文無關的。在編譯原理中,將描述語言構造的層次化語法結構稱爲「文法」(grammar),咱們的十之內的四則運算算式就是一個「上下文無關文法」(context-free grammar)。編譯原理中定義了上下文無關文法由四個元素構成:
語法分析樹是一種圖形表示,他展示了從文法的開始符號推導出相應語言中的終結符號串的過程。例如一個給定一個算式:9 - 5 + 2,能夠表示成以下的語法分析樹:
expr expr + expr expr - expr digit digit digit 2 9 5
單純從 9 - 5 + 2 出發去畫語法分析樹,還能獲得另外一種結果,以下:
expr expr - expr digit expr + expr 9 digit digit 5 2
若是咱們從下往上對語法分析樹進行計算,前一棵樹先計算 9 - 5 得 4,而後 4 + 2 得 6,但後一棵樹的結果則是 5 + 2 得 7,9 - 7 得 2。這就是文法得二義性,其定義爲:對於同一個給定的終結符號串,有兩棵及以上的語法分析樹。因爲多棵樹意味着多個含義,咱們須要設計沒有二義性的文法,或給二義性文法添加附加規則來對齊進行消除。
在本例中,咱們採用設計文法的方式來消除二義性。因爲四則運算中,加減位於一個優先級層次,乘除位於另外一個,咱們建立兩個非終結符號expr
和term
分別對應這兩個層次,並使用另外一個非終結符號factor
來生成表達式中的基本單元,可獲得以下的產生式:
factor ---> digit | (expr) // 考慮乘法和加法的左結合性 term ---> term * factor | term / factor | factor expr ---> expr + term | expr - term | term
有了新的文法以後,咱們再看 9 - 5 + 2,其僅能生成以下的惟一語法分析樹:
expr expr + term expr - term factor term factor digit factor digit 2 digit 5 9
如今咱們回到San中的表達式,有了前面的基礎,相信你們都已經清楚了parseExpr
解析表達式源字符串方法的原因。接下來,咱們只要合理的定義出來「San中的表達式」這一語言的產生式,函數實現就水到渠成了。
表達式解析入口parseExpr:
/** * 解析表達式 * * @param {string} source 源碼 * @return {Object} */ function parseExpr(source) { if (typeof source === 'object' && source.type) { return source; } var expr = readTertiaryExpr(new Walker(source)); expr.raw = source; return expr; }
其對應的產生式就是:
Expr ---> TertiaryExpr
/** * 讀取三元表達式 * * @param {Walker} walker 源碼讀取對象 * @return {Object} */ function readTertiaryExpr(walker) { var conditional = readLogicalORExpr(walker); walker.goUntil(); if (walker.currentCode() === 63) { // ? walker.go(1); var yesExpr = readTertiaryExpr(walker); walker.goUntil(); if (walker.currentCode() === 58) { // : walker.go(1); return { type: ExprType.TERTIARY, segs: [ conditional, yesExpr, readTertiaryExpr(walker) ] }; } } return conditional; }
能夠看到,判斷條件部分conditional
是readLogicalORExpr
的結果。若是存在?
、:
兩個和三元表達式相關的終結符號,就返回一個三元表達式類型的表達式對象;不然直接返回conditional
。可知產生式:
TertiaryExpr ---> LogicalORExpr ? TertiaryExpr : TertiaryExpr | LogicalORExpr
由readLogicalORExpr可得產生式:
LogicalORExpr ---> LogicalORExpr || LogicalANDExpr | LogicalANDExpr
LogicalANDExpr ---> LogicalANDExpr && EqualityExpr | EqualityExpr
EqualityExpr ---> RelationalExpr == RelationalExpr | RelationalExpr != RelationalExpr | RelationalExpr === RelationalExpr | RelationalExpr !== RelationalExpr | RelationalExpr
RelationalExpr ---> AdditiveExpr > AdditiveExpr | AdditiveExpr < AdditiveExpr | AdditiveExpr >= AdditiveExpr | AdditiveExpr <= AdditiveExpr | AdditiveExpr
/** * 讀取加法表達式 * * @param {Walker} walker 源碼讀取對象 * @return {Object} */ function readAdditiveExpr(walker) { var expr = readMultiplicativeExpr(walker); while (1) { walker.goUntil(); var code = walker.currentCode(); switch (code) { case 43: // + case 45: // - walker.go(1); // 這裏建立了一個新對象,包住了原來的 expr,返回了一個新的 expr expr = { type: ExprType.BINARY, operator: code, segs: [expr, readMultiplicativeExpr(walker)] }; // 注意到這裏是 continue,以前的函數都是 return continue; } break; } return expr; }
讀加法的這個函數有些特殊,其在第一步先調用了讀乘法的方法,獲得了變量expr
,而後不斷地更新expr
對象包住原來的對象,以保證結合性的正確。
方法的產生式以下:
AdditiveExpr ---> AdditiveExpr + MultiplicativeExpr | AdditiveExpr - MultiplicativeExpr | MultiplicativeExpr
MultiplicativeExpr ---> MultiplicativeExpr * UnaryExpr | MultiplicativeExpr / UnaryExpr | MultiplicativeExpr % UnaryExpr | UnaryExpr
readUnaryExpr這個函數,包含了除布爾值的表達式以外的,各個表示數據得表達式的解析部分。所以對應的產生式也相對複雜,爲了便於說明,我自行引入了一些非終結符號:
UnaryExpr ---> !UnaryExpr | 'String' | "String" | Number | ArrayLiteral | ObjectLiteral | ParenthesizedExpr | Accessor ArrayLiteral ---> [] | [ElementList] // 這裏引入一個新的非終結符號 ElementList 來輔助說明 ElementList ---> Element | ElementList, Element Element ---> TertiaryExpr | ...TertiaryExpr ObjectLiteral ---> {} | {FieldList} // 相似上面的 ElementList FieldList ---> Field | FieldList, Field Field ---> ...TertiaryExpr | SimpleExpr | SimpleExpr: TertiaryExpr SimpleExpr ---> true | false | 'String' | "String" | Number
ParenthesizedExpr ---> (TertiaryExpr)
由readAccessor得:
Accessor ---> true | false | Identifier MemberOperator* // 此處 * 表示 0個或多個的意思 MemberOperator ---> .Identifier | [TertiaryExpr]
至此,咱們終於把全部的產生式都梳理清楚了。
在這裏我附上一份JavaScript 1.4 Grammar供參考。經過對比兩種文法產生式的不一樣,能找到不少二者之間解析結果得差別。下面是一個例子:
1 > 2 < 3 // 返回 true,至關於 1 > 2 返回 false,false < 3 返回 true san.evalExpr(san.parseExpr('1 > 2 < 3'), new san.Data()); // 返回 false
注意到 San 中關於RelationalExpression
的產生式是:
RelationalExpr ---> AdditiveExpr > AdditiveExpr | AdditiveExpr < AdditiveExpr | AdditiveExpr >= AdditiveExpr | AdditiveExpr <= AdditiveExpr | AdditiveExpr
也就是說,對於1 > 2 < 3
,其匹配了RelationalExpr ---> AdditiveExpr > AdditiveExpr
。其中1
傳入了AdditiveExpr
解析成Number
的1
;2 < 3
則被視爲另外一個AdditiveExpr
進行解析,因爲後面已經沒有可以處理<
的邏輯了,因此會被解析成Number
的2
。因此,輸入的1 > 2 < 3
,真正解析出來的就只有1 > 2
了,因此上面的代碼會返回 false 。
我的認爲 San 在這裏應該是刻意爲之的。由於對於1 > 2 < 3
這種表達式,真的不必保證它按照JavaScript
的文法來解析——這種代碼寫出來確定是要改的,沒有顧及它的意義。
瞭解了 parseExpr 是如何從源字符串獲得表達式對象以後,也就發現其實不少地方都用了相似的方法來描述語法。好比CSS 線性漸變。這裏個人連接直接指向了MDN上關於線性漸變的形式語法(Formal syntax)部分,能夠看到這部分對線性漸變語法的描述,和我上面解析 parseExpr 的時候所用的產生式一模一樣。
linear-gradient( [ <angle> | to <side-or-corner> ,]? <color-stop> [, <color-stop>]+ ) \---------------------------------/ \----------------------------/ Definition of the gradient line List of color stops where <side-or-corner> = [left | right] || [top | bottom] and <color-stop> = <color> [ <percentage> | <length> ]?
這種語法形式是MDN定義的CSS屬性值定義語法。
參照咱們前面所寫的產生式與上面的CSS屬性值定義語法,我寫出了以下的產生式:
expr ---> gradientLine , colorStopList | colorStopList gradientLine ---> angle | to sideOrCorner sideOrCorner ---> horizon | vertical | horizon vertical | vertical horizon horizon ---> left | right vertical ---> top | bottom colorStopList ---> colorStopList, color distance | color distance color ---> hexColor | rgbColor | rgbaColor | literalColor | hslColor // 相信你們都懂,我就不作進一步展開了 distance ---> percentage | length // 同上,不作進一步展開
這一趟下來能夠說是補了很多課,也揭示了 San 中內部原理的一角,後面計劃把 evalExpr
、Data
、parseTemplate
等方法也學習一遍,進一步瞭解 San 的全貌。