表达式设计与实现
概述
VN的表达式引擎使用胡子语法(Mustache)。Mustache 是一个 logic-less(轻逻辑)模板解析引擎,它是为了使用户界面与业务数据(内容)分离而产生的。比如 vnml 中的代码:{{userInfo.nickName}}
,这里的{{
与}}
包含的内容就是 Mustache的语法。
能力
目前VN的表达式语法支持主流的数据表示方法,各种基本的运算符还支持函数。具体内容如下:
数据表示
数据类型 | 样例 | 说明 |
---|---|---|
Boolean | true,false | |
Integer | 1,5 | |
Long | 1L,5l | |
Double | 1d,5D | |
String | 'Hello',"World" | |
ID | dataArray[5],dataObj.name | 标识符在运算时从页面的数据中获取实际值 |
运算符
运算类型 | 样例 | 说明 |
---|---|---|
+,-,*,/,% | 1+2 | 算数运算符支持数字类型的数据运算 |
(,) | (1+2)*3 | 用于提高运算优先级 |
<,<=,>,>=,==,!=,&& | 2<5 | 逻辑运算符,运算结果是Boolean类型 |
&,~,^ | 2&5 | 位运算符 |
: ? | 2>3 ? 2:3 | 三元运算符 |
函数
函数类型 | 样例 | 入参 | 返回值 |
---|---|---|---|
max | max(1,2) | 数值类型 | 返回其中最大的一个 |
min | min(1,2) | 数值类型 | 返回其中最小的一个 |
trim | trim(" Hello World ") | 一个字符串 | 返回去掉两端空格后的字符串 |
toRpx | toRpx("15pt") | 单位字符串 | 返回转化为RPX单位的float值 |
主流程
Token解析
Token解析的第一步是将表达式字符串转化为操作符,标识符,数字,字符串这四类Token。因为大部分Token解析的过程不需要依赖上下文所以处理过程比较简单。这里唯一特殊的就是对函数的处理,如果连续两个Token为标识符和左括号那么就记为方法的开始,当收到下一个右括号的时候记为方法的结束。
- 操作符:算数运算符,位运算符,逻辑运算符,其他(
’.’运算符, ‘[ ]’运算符,’,’运算符,’(’运算符,’)’运算符等
) - 标识符:变量,函数名,函数参数名称等
- 数字:Long,Int,Double,Float
- 字符串
识别出四种Token之后需要根据上下文做一些简单的处理:
- 区分负号和减号操作符,方法是看前一个操作符是否为数字
- 区分函数名,函数参数和变量,函数名是方法开始前面的那个标识符,函数参数是方法开始到方法结束中间的标识符,其余标识符都是变量。
调度场算法
解析完Token之后下一步就是将符合人类习惯的中缀表达式转换为计算机更好处理的后缀表达式(逆波兰表达式)的过程了。它的特点有:
- 操作符置于被操作数的后面;
- 不需要括号,也不需要定义优先级,只需从左到右依次计算即可。
调度场算法具体的执行过程如下:
读入一个Token,直到无Token可读
如果Token是操作数(标识符,字符串,数字),则加入输出队列
如果Token是操作符,记作 Oc
若Oc的优先级低于栈顶或Oc的优先级等于栈顶且Oc有左结合性,则弹栈入列,直到条件被打破 *
否则,Oc 压栈
如果Token是左括号,则压栈
如果Token是右括号,则
弹栈入列,直到遇见左括号
弹栈,丢弃左括号
若遇见左括号之前,栈为空,则括号不匹配(右括号多)
无Token可读时
弹栈入列,直至栈空
若栈空之前遇见左括号,则括号不匹配(左括号多)
注:对条件运算符(即'? :'这个三元运算符)我们要进行一些特殊处理。具体方法是:条件运算符优先级很低,只比’,’操作符的优先级高,’?’与':'操作符满足右结合性, 在标记为'*'的那个步骤中当 ‘:’操作符匹配了一个’?’将其出栈并停止循环。
下面我用一个特殊的例子说明一下:
1 + 2 * 3 > 4 ? 5 : 6 > 7 ? 8 : 9
整个调度场算法的执行过程梳理如下:
输入 | 动作 | 输出 | 操作符栈 | 说明 |
---|---|---|---|---|
1 | 将操作数加入输出队列 | 1 | ||
+ | 操作符压栈 | 1 | + | |
2 | 将操作数加入输出队列 | 12 | + | |
* | 操作符压栈 | 12 | +* | 当前操作符优先级高于栈顶 |
3 | 将操作数加入输出队列 | 123 | +* | |
> | 操作符弹栈入列 | 123* | + | 当前操作符优先级低于栈顶 |
同上 | 操作符弹栈入列 | 123*+ | 当前操作符优先级低于栈顶 | |
同上 | 操作符压栈 | 123*+ | > | |
4 | 将操作数加入输出队列 | 123*+4 | > | |
? | 操作符弹栈入列 | 123*+4> | 当前操作符优先级低于栈顶 | |
同上 | 操作符压栈 | 123*+4> | ? | |
5 | 将操作数加入输出队列 | 123*+4>5 | ? | |
: | 操作符弹栈入列 | 123*+4>5? | ':'操作符匹配了栈顶的'?'操作符 | |
同上 | 操作符压栈 | 123*+4>5? | : | |
6 | 将操作数加入输出队列 | 123*+4>5?6 | : | |
> | 操作符压栈 | 123*+4>5?6 | :> | 当前操作符优先级高于栈顶 |
7 | 将操作数加入输出队列 | 123*+4>5?67 | :> | |
? | 将操作符弹栈入列 | 123*+4>5?67> | : | 当前操作符优先级低于栈顶 |
同上 | 操作符压栈 | 123*+4>5?67> | :? | '?'与':'操作符的优先级相同,且右结合所以压栈 |
8 | 将操作数加入输出队列 | 123*+4>5?67>8 | :? | |
: | 操作符弹栈入列 | 123*+4>5?67>8? | : | ':'操作符匹配了栈顶的'?'操作符 |
同上 | 操作符压栈 | 123*+4>5?67>8? | :: | |
9 | 将操作数加入输出队列 | 123*+4>5?67>8?9 | :: | |
EOL | 操作符弹栈入列至栈空 | 123*+4>5?67>8?9:: | 操作符弹栈入列至栈空 |
生成操作树
生成操作树的过程需要将后缀表达式转化为一个有操作符和操作数作为节点的树。转化的过程如下:
读入一个Token,直到无Token可读 如果该Token为操作数直接入栈; 如果该Token为操作符pop操作符对应的参数个数作为操作符的孩子节点再入栈。
最终如果表达式合法的话栈中应该会只有一个操作符元素,改元素就是操作树的根。
这里举一个简单一些的例子:
1+2*3
它的后缀表达式的形式为
123*+
生成的操作树为如下
计算与结果输出
计算和结果输出比较简单,这里需要注意的就是表达式中允许变量的出现,我们在实现的时候通过接口注入的方法在运行表达式的时候实时获取变量的值。另外,就是我们大部分的操作符都会兼容不同的数据类型在实施操作之前将两个操作数(对于二元运算符)转化为相同的类型再运算。