博客源地址:
https://github.com/LeuisKen/l…相干批評還請到 issue 下。
要領申明
san.parseExpr是San中主模塊下的一個要領。用於將源字符串剖析成表達式對象。該要領和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。這就是文法得二義性,其定義為:關於同一個給定的閉幕標記串,有兩棵及以上的語法剖析樹。由於多棵樹意味着多個寄義,我們須要設想沒有二義性的文法,或給二義性文法增加附加劃定規矩來對齊舉行消弭。
在本例中,我們採納設想文法的體式格局來消弭二義性。由於四則運算中,加減位於一個優先級條理,乘除位於另一個,我們建立兩個非閉幕標記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
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
/**
* 讀取三元表達式
*
* @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 文法的對照
在這裏我附上一份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 的全貌。