缘起
在google上搜索 “vue不能兼容IE8”,在知乎上看到Yox的作者在推他写的这个框架。这个框架的特点是属性方法设计基本与vue
一致,模板语法参照handlebar
,能兼容IE6以上的浏览器。
值得一提的是,Yox的作者对自己的作品很有自信,且声称该框架一直用在自己的工作中。
阅读框架源码,发觉他的自信是有道理的,理由有:
- 代码使用typescript编写
- 代码组织清晰,变量命名简单易懂,少新造概念,并且有足够的注释
- 有独特的设计,如使用
handlebar
的模板语法,在列表渲染中有类似变量作用域的语法设计("../name"
表示使用上一层的name属性) - 整个框架基本由他一人开发
因为有这些特点,让我觉得这是一个值得深入学习的项目。
正题
模板编译的工作是把模板字符串转成函数代码,我看过的一些模板引擎ejs
,pug
,vue
,yox
都是同样的做法。yox
把模板表达式的编译部分拆分成独立的模块(yox-expression-compiler
),整个模板的编译为yox-template-compiler
模块。
yox-expression-compiler
模块包含了三个感觉比较重要的概念:
- compiler:解析表达式
- creator:创建组成表达式的各类节点,如字面量、标识符、函数调用节点
节点,其实就是结构对象:1
2
3
4
5
6
7function createLiteral(value: any, raw: string): Literal {
return {
type: nodeType.LITERAL,
raw,
value,
}
}
- generator: 将节点转换成代码字符
creator和generator相对来说没那么复杂,而compiler则负责代码扫描解析逻辑的任务。
compiler的设计
- 游标移动
go
:前进后退skip
: 跳过空白字符
- token类型判断——
scanToken
,有下列情况:- identifier(标识符,如
a
,name
) - literal(字面量)
- number
- 字符串
- 数组
- 对象
- 一元运算符(二元运算符的提取会在
scanBinary
) - 特殊字符
(xx)
,括号.
,../
,表示上面提到的作用域切换或者'.'
开头的数字
- identifier(标识符,如
- 运算式的解析——
scanTernary
、scanBinary
其他规则:
- 当遇到idenfier或者字面量(除number和对象外),还会进行
scanTail
逻辑(意思式检测后面是否接着.
,[]
这样的取成员的表达式以及(a,b,c)
这样可能的函数调用表示,如果有则会组成一个新的节点) - 当下一个接的值是可能的任意值时(如对象的属性值,数组的成员,函数参数),都会调用
scanTernary
,所以在启动编译时,第一步就是执行这个方法。可能的解释是三元表达式是包含内联代码所有可能的表达式,所以先假定是三元表达式,如果不符合条件再fallback到其余情况。
比较有启发性的解析方法:
- 对象解析:
对象分为key和value,所以在解析时会在key
和value
两个模式中进行切换,初始时key
,遇到:
转value
,遇到,
转key
。解析到key
和value
时,会分别添加到keys
和values
数组。当遇到}
闭合字符时,根据两个数组中成员的个数,判断对象是否合法。 二元运算式解析:
运算式由运算数(operand)和运算符(operator)组成,先解析运算数后解析运算符,而二元运算符有优先级的问题(a + b * c + d
,应该先运算b * c
)。解决的方法为源码中提到的Shunting-yard algorithm
中心思想是,运算数和运算符会按顺序
push
到数组中,确保扫描到的当前运算符的优先级小于前一个运算符,否则则将前一个二元运算提出来作为一个新的Node
(如上面+ d
的+
)(中文译作调度场算法,一种将中缀表达式
a + b
转成后缀表达a b +
的算法,之前好像有在线上课程中提到过,应该是计算机系的课程内容。wiki上说中缀表达式不易被电脑识别,但感觉这不构成在这里使用这个算法的理由,毕竟似乎 “假如当前运算符的优先级高于前一个,则将后面的二元运算式提出”也行得通。不过可能这样的话,由于还不知道下一个操作数,不好做处理,所以选用了这个算法)。
当后面不再有二元运算式时,再从数组后面取出组成一个个的二元运算式节点。
compiler的阅读体会
- 对于代码表达式来说,通常都会有开始标志和结束标志的这样成对的设计,如html标签
<div></div>
,字符串'a'
,"abc"
,对象{ a: 1 }
,数组[ 1, 2, 3 ]
,函数参数(1, 2, 3)
。 - 之前觉得像对象那样的嵌套结构挺难处理的,但事实上这种情况完全符合递归的场景,只需要再调用根函数即可,像上面说的
scanTernary
后记
下一篇应该是关于整个template
编译的学习。现在开始需要多学习框架或者大项目的设计模式,这样才能学会独立从零到一开发项目。