san.parseExpr 源碼學習

博客源地址: https://github.com/LeuisKen/l...
相關評論還請到 issue 下。

方法說明

san.parseExprSan中主模塊下的一個方法。用於將源字符串解析成表達式對象。該方法和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

  • TertiaryExpr:三元表達式
  • LogicalORExpr:邏輯或
  • LogicalANDExpr:邏輯與
  • EqualityExpr:判等
  • RelationalExpr:關係(大於、小於等)
  • AdditiveExpr:加減法
  • MultiplicativeExpr:乘除法、取餘運算
  • UnaryExpr:一元表達式
  • ParenthesizedExpr:括號表達式

除了上述表示運算關係的表達式外,還有表示數據的表達式,以下:git

  • String:字符串
  • Number:數組
  • Boolean:布爾值
  • ArrayLiteral:數組字面量
  • ObjectLiteral:對象字面量
  • Accessor:訪問器表達式

因爲Accessor存在乎義,是爲了在evalExpr階段從Data對象中獲取數據,因此這裏我將Accessor歸類爲表示數據的表達式。github

如今咱們知道了全部的表達式類型,那麼,parseExpr是如何從字符串中,解析出表達式對象的呢?正則表達式

如何讀取字符串

parseExpr方法定義在src/parser/parse-expr.js中。咱們能夠看到其依賴了一個Walker類,註釋中的說明是字符串源碼讀取類。數組

Walker類包含如下內容:ide

屬性:

  • this.source:保存要讀取的源字符串
  • this.len:保存源字符串長度
  • this.index:保存當前對象讀取字符的位置

方法:

  • currentCode方法:返回當前讀取字符的 charCode
  • charCode方法:返回指定位置字符的 charCode
  • cut方法:根據指定起始和結束位置返回字符串片斷
  • go方法:將this.index增長給定數值
  • nextCode方法:讀取下一個字符並返回它的 charCode

goUntil 方法

/**
 * 向前讀取字符,直到遇到指定字符再中止
 * 未指定字符時,當遇到第一個非空格、製表符的字符中止
 *
 * @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;
        }
    }
};

match 方法

/**
 * 向前讀取符合規則的字符片斷,並返回規則匹配結果
 *
 * @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)。編譯原理中定義了上下文無關文法由四個元素構成:

  • 終結符號集合
  • 非終結符號集合
  • 產生式集合
  • 一個指定的非終結符號做爲開始符號(上面的expr)

語法分析樹

語法分析樹是一種圖形表示,他展示了從文法的開始符號推導出相應語言中的終結符號串的過程。例如一個給定一個算式: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。這就是文法得二義性,其定義爲:對於同一個給定的終結符號串,有兩棵及以上的語法分析樹。因爲多棵樹意味着多個含義,咱們須要設計沒有二義性的文法,或給二義性文法添加附加規則來對齊進行消除。

在本例中,咱們採用設計文法的方式來消除二義性。因爲四則運算中,加減位於一個優先級層次,乘除位於另外一個,咱們建立兩個非終結符號exprterm分別對應這兩個層次,並使用另外一個非終結符號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

parseExpr 的實現

如今咱們回到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

readTertiaryExpr

/**
 * 讀取三元表達式
 *
 * @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;
}

能夠看到,判斷條件部分conditionalreadLogicalORExpr的結果。若是存在?:兩個和三元表達式相關的終結符號,就返回一個三元表達式類型的表達式對象;不然直接返回conditional。可知產生式:

TertiaryExpr ---> LogicalORExpr ? TertiaryExpr : TertiaryExpr
                | LogicalORExpr

readLogicalORExpr可得產生式:

LogicalORExpr ---> LogicalORExpr || LogicalANDExpr
                 | LogicalANDExpr

readLogicalANDExpr得:

LogicalANDExpr ---> LogicalANDExpr && EqualityExpr
                  | EqualityExpr

readEqualityExpr得:

EqualityExpr ---> RelationalExpr == RelationalExpr
                | RelationalExpr != RelationalExpr
                | RelationalExpr === RelationalExpr
                | RelationalExpr !== RelationalExpr
                | RelationalExpr

readRelationalExpr得:

RelationalExpr ---> AdditiveExpr > AdditiveExpr
                  | AdditiveExpr < AdditiveExpr
                  | AdditiveExpr >= AdditiveExpr
                  | AdditiveExpr <= AdditiveExpr
                  | AdditiveExpr

readAdditiveExpr

/**
 * 讀取加法表達式
 *
 * @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

readMultiplicativeExpr得:

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

readParenthesizedExpr得:

ParenthesizedExpr ---> (TertiaryExpr)

readAccessor得:

Accessor ---> true
            | false
            | Identifier MemberOperator*        // 此處 * 表示 0個或多個的意思

MemberOperator ---> .Identifier
                  | [TertiaryExpr]

至此,咱們終於把全部的產生式都梳理清楚了。

和 JavaScript 文法的對比

在這裏我附上一份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解析成Number12 < 3則被視爲另外一個AdditiveExpr進行解析,因爲後面已經沒有可以處理<的邏輯了,因此會被解析成Number2。因此,輸入的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 中內部原理的一角,後面計劃把 evalExprDataparseTemplate等方法也學習一遍,進一步瞭解 San 的全貌。

相關文章
相關標籤/搜索