san.parseExpr 源碼進修

博客源地址:
https://github.com/LeuisKen/l…

相干批評還請到 issue 下。

要領申明

san.parseExprSan中主模塊下的一個要領。用於將源字符串剖析成表達式對象。該要領和san.evalExpr是一對,後者吸收一個表達式對象和一個san.Data對象作為參數,用於對表達式舉行求值。以下例:

/**
 * 剖析表達式
 *
 * @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須要剖析的表達式都有哪些:

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

除了上述示意運算關聯的表達式外,另有示意數據的表達式,以下:

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

由於Accessor存在意義,是為了在evalExpr階段從Data對象中獵取數據,所以這裏我將Accessor歸類為示意數據的表達式。

如今我們曉得了一切的表達式範例,那末,parseExpr是怎樣從字符串中,剖析出表達式對象的呢?

怎樣讀取字符串

parseExpr要領定義在src/parser/parse-expr.js中。我們可以看到其依靠了一個Walker類,解釋中的申明是字符串源碼讀取類。

Walker類包含以下內容:

屬性:

  • 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 的全貌。

    原文作者:LeuisKen
    原文地址: https://segmentfault.com/a/1190000015321952
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞