15.3 构造AST
用一个栈顺序扫描 Token 列表,就能把线性的 Token 还原成树形的模板 AST。
一、思路:扫描 + 栈
- 从第一个 Token 开始顺序扫描整个 Token 列表,直到所有 Token 处理完毕。
- 维护一个栈
elementStack来记录元素间的父子关系:- 遇到开始标签 Token:构造
Element类型的 AST 节点,把它压入栈; - 遇到结束标签 Token:弹出栈顶节点;
- 栈顶节点始终充当当前父节点,扫描到的节点都追加进它的
children; - 栈底永远只有
Root根节点。
- 遇到开始标签 Token:构造
二、节点类型
| type | 描述 | 属性 |
|---|---|---|
Root | 模板根节点 | children |
Element | 标签节点 | tag、children |
Text | 文本节点 | content |
「子节点存在父节点的
children数组里」这一约定贯穿整章:后面做节点替换、删除,改的都是父节点的children。
三、parse 的实现
function parse(str) {
// 1. 先对模板做标记化,得到 tokens
const tokens = tokenize(str)
// 2. 创建 Root 根节点
const root = { type: 'Root', children: [] }
// 3. 创建 elementStack 栈,起初只有 Root 根节点
const elementStack = [root]
// 4. 循环扫描 tokens,直到全部处理完毕
while (tokens.length) {
// 当前栈顶节点即父节点
const parent = elementStack[elementStack.length - 1]
// 当前扫描的 Token
const t = tokens[0]
switch (t.type) {
case 'tag': {
// 开始标签 → 创建 Element 节点
const elementNode = { type: 'Element', tag: t.name, children: [] }
parent.children.push(elementNode) // 挂到父节点下
elementStack.push(elementNode) // 自己成为后续节点的父节点
break
}
case 'text': {
// 文本 → 创建 Text 节点
const textNode = { type: 'Text', content: t.content }
parent.children.push(textNode)
break
}
case 'tagEnd':
// 结束标签 → 弹出栈顶节点
elementStack.pop()
break
}
tokens.shift() // 消费已经扫描过的 Token
}
return root
}
四、效果
parse('<div>hel<p>vue</p>lo</div>') 得到的 AST:
Root
└── Element: div
├── Text: hel
├── Element: p
│ └── Text: vue
└── Text: lo
易错点:
Element节点必须先 push 进父节点的children、再压栈;顺序反了,p会变成div的兄弟而不是子节点。
相关
- 15.2 parser 的实现原理与状态机:Token 是怎么切出来的
- 15.4 AST 的转换与插件化架构:拿到 AST 之后如何转换