16.2 递归下降算法构造模板 AST
解析器 parser 的骨架:用
parseChildren这个状态机不断地消费模板字符串,靠递归下降把嵌套的标签解析成树形 AST。
一、parseChildren 是解析器的核心
parseChildren函数会递归地调用自己来不断消费模板内容;- 它本质上也是一个状态机,该状态机有多少种状态,取决于子节点的类型数量。
模板中元素的子节点可能是:
| 子节点类型 | 例子 | 解析函数 |
|---|---|---|
| 标签节点 | <div> | parseElement |
| 注释节点 | <!-- comments --> | parseComment |
| CDATA 节点 | <![CDATA[ xxx ]]> | parseCDATA |
| 文本插值节点 | {{ val }} | parseInterpolation |
| 普通文本节点 | text | parseText |
二、状态迁移过程
- 当遇到字符
<时,进入临时状态:- 下一个字符匹配
/a-z/i→ 认为是标签节点,调用parseElement; - 字符串以
<!--开头 → 认为是注释节点,调用parseComment; - 字符串以
<![CDATA[开头 → 认为是 CDATA 节点,调用parseCDATA; - 字符串以
/开头 → 是结束标签(在 DATA 模式下出现即说明缺少对应的开始标签,见 16.3);
- 下一个字符匹配
- 字符串以
{{开头 → 认为是插值节点,调用parseInterpolation; - 其他情况一律作为普通文本,调用
parseText。
三、模式对可解析节点的限制
- 只有处于 DATA 模式或 RCDATA 模式时,解析器才支持插值节点;
- 只有处于 DATA 模式时,解析器才支持标签节点、注释节点和 CDATA 节点;
- 一旦切换到 DATA / RCDATA 之外的模式,一切字符都作为文本节点解析;
- 即使在 DATA / RCDATA 模式下,如果无法匹配上面任何一种节点,也仍然作为文本节点解析。
四、递归下降的含义
- 调用
parseElement解析标签,会间接地调用parseChildren,从而产生一个新的状态机; - 标签嵌套层次越深,新的状态机就随着
parseChildren被递归调用而不断创建,这就是「递归下降」中递归二字的含义; - 上级
parseChildren用于构造上级 AST 节点,被递归调用的下级parseChildren用于构造下级 AST 节点,最终构造出一棵树形结构的模板 AST; - 返回值是由子节点组成的数组:每次
while循环解析一个或多个节点,压入nodes后作为返回值返回。
五、解析器骨架
function parse(str) {
const context = {
source: str, // 模板内容,解析过程中不断被消费
mode: TextModes.DATA // 当前文本模式,初始为 DATA
}
// 第二个参数是由父代节点构成的节点栈,初始为空
const nodes = parseChildren(context, [])
return {
type: 'Root',
children: nodes
}
}
function parseChildren(context, ancestors) {
let nodes = []
const { mode, source } = context
while (!isEnd(context, ancestors)) {
let node
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (mode === TextModes.DATA && source[0] === '<') {
if (source[1] === '!') {
if (source.startsWith('<!--')) {
node = parseComment(context)
} else if (source.startsWith('<![CDATA[')) {
node = parseCDATA(context, ancestors)
}
} else if (source[1] === '/') {
// 结束标签:缺少对应的开始标签,报错
} else if (/[a-z]/i.test(source[1])) {
node = parseElement(context, ancestors)
}
} else if (source.startsWith('{{')) {
node = parseInterpolation(context)
}
}
// 非 DATA / RCDATA 模式,或未匹配任何节点:按文本处理
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
易错点:
parseChildren一定要保证每轮循环都消费掉一部分内容,否则while会永远转下去。
相关
- 16.3 状态机的开启与停止:
isEnd与父级节点栈如何决定状态机何时结束 - 16.4 解析标签节点:
parseElement与parseTag的实现 - 16.6.1 解析文本:
parseText如何确定文本的结尾 - 16.7 解析插值与注释:
parseInterpolation与parseComment