博客源地址: 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 的全貌。