a house in the woods

Hi, nice to meet you.

  1. 1. 缘起
  2. 2. 正题
    1. 2.1. compiler的设计
      1. 2.1.1. 其他规则:
      2. 2.1.2. 比较有启发性的解析方法:
  3. 3. compiler的阅读体会
  4. 4. 后记

缘起

在google上搜索 “vue不能兼容IE8”,在知乎上看到Yox的作者在推他写的这个框架。这个框架的特点是属性方法设计基本与vue一致,模板语法参照handlebar,能兼容IE6以上的浏览器。
值得一提的是,Yox的作者对自己的作品很有自信,且声称该框架一直用在自己的工作中。

阅读框架源码,发觉他的自信是有道理的,理由有:

  1. 代码使用typescript编写
  2. 代码组织清晰,变量命名简单易懂,少新造概念,并且有足够的注释
  3. 有独特的设计,如使用handlebar的模板语法,在列表渲染中有类似变量作用域的语法设计("../name"表示使用上一层的name属性)
  4. 整个框架基本由他一人开发

因为有这些特点,让我觉得这是一个值得深入学习的项目。

正题

模板编译的工作是把模板字符串转成函数代码,我看过的一些模板引擎ejspugvueyox都是同样的做法。
yox把模板表达式的编译部分拆分成独立的模块(yox-expression-compiler),整个模板的编译为yox-template-compiler模块。

yox-expression-compiler模块包含了三个感觉比较重要的概念:

  1. compiler:解析表达式
  2. creator:创建组成表达式的各类节点,如字面量、标识符、函数调用节点

节点,其实就是结构对象:

1
2
3
4
5
6
7
function createLiteral(value: any, raw: string): Literal {
return {
type: nodeType.LITERAL,
raw,
value,
}
}

  1. generator: 将节点转换成代码字符

creator和generator相对来说没那么复杂,而compiler则负责代码扫描解析逻辑的任务。

compiler的设计

  1. 游标移动
    1. go:前进后退
    2. skip: 跳过空白字符
  2. token类型判断——scanToken,有下列情况:
    1. identifier(标识符,如a, name
    2. literal(字面量)
      1. number
      2. 字符串
      3. 数组
      4. 对象
    3. 一元运算符(二元运算符的提取会在scanBinary
    4. 特殊字符
      1. (xx),括号
      2. .,../,表示上面提到的作用域切换或者'.'开头的数字
  3. 运算式的解析——scanTernaryscanBinary
其他规则:
  1. 当遇到idenfier或者字面量(除number对象外),还会进行scanTail逻辑(意思式检测后面是否接着.[]这样的取成员的表达式以及(a,b,c)这样可能的函数调用表示,如果有则会组成一个新的节点)
  2. 当下一个接的值是可能的任意值时(如对象的属性值,数组的成员,函数参数),都会调用scanTernary,所以在启动编译时,第一步就是执行这个方法。可能的解释是三元表达式是包含内联代码所有可能的表达式,所以先假定是三元表达式,如果不符合条件再fallback到其余情况。
比较有启发性的解析方法:
  1. 对象解析:
    对象分为key和value,所以在解析时会在keyvalue两个模式中进行切换,初始时key,遇到:value,遇到,key。解析到keyvalue时,会分别添加到keysvalues数组。当遇到}闭合字符时,根据两个数组中成员的个数,判断对象是否合法。
  2. 二元运算式解析:
    运算式由运算数(operand)运算符(operator)组成,先解析运算数后解析运算符,而二元运算符有优先级的问题(a + b * c + d,应该先运算b * c)。

    解决的方法为源码中提到的Shunting-yard algorithm

    中心思想是,运算数运算符会按顺序push到数组中,确保扫描到的当前运算符的优先级小于前一个运算符,否则则将前一个二元运算提出来作为一个新的Node(如上面+ d+

    (中文译作调度场算法,一种将中缀表达式a + b转成后缀表达a b +的算法,之前好像有在线上课程中提到过,应该是计算机系的课程内容。

    wiki上说中缀表达式不易被电脑识别,但感觉这不构成在这里使用这个算法的理由,毕竟似乎 “假如当前运算符的优先级高于前一个,则将后面的二元运算式提出”也行得通。不过可能这样的话,由于还不知道下一个操作数,不好做处理,所以选用了这个算法)。

    当后面不再有二元运算式时,再从数组后面取出组成一个个的二元运算式节点。

compiler的阅读体会

  1. 对于代码表达式来说,通常都会有开始标志和结束标志的这样成对的设计,如html标签<div></div>,字符串'a',"abc",对象{ a: 1 },数组[ 1, 2, 3 ],函数参数(1, 2, 3)
  2. 之前觉得像对象那样的嵌套结构挺难处理的,但事实上这种情况完全符合递归的场景,只需要再调用根函数即可,像上面说的scanTernary

后记

下一篇应该是关于整个template编译的学习。现在开始需要多学习框架或者大项目的设计模式,这样才能学会独立从零到一开发项目。

This article was last updated on days ago, and the information described in the article may have changed.