15.4.1 节点的访问
要修改 AST,先得能访问到每一个节点——用深度优先遍历;再把「遍历」与「转换逻辑」解耦成可注册的插件。
一、遍历工具:dump 与 traverseNode
- AST 是树形数据结构,要访问所有节点就得写深度优先的遍历算法。
- 先写一个
dump工具函数,把 AST 中节点的信息打印出来,便于调试。
// 打印 AST 中节点的信息
function dump(node, indent = 0) {
const type = node.type
// 描述:Root 没有描述;Element 用 tag;Text 用 content
const desc = node.type === 'Root'
? ''
: node.type === 'Element'
? node.tag
: node.content
console.log(`${'-'.repeat(indent)}${type}: ${desc}`)
// 递归打印子节点
if (node.children) {
node.children.forEach(n => dump(n, indent + 2))
}
}
二、第一版:转换逻辑写死在遍历里
function traverseNode(ast) {
const currentNode = ast
// 1. 把所有 p 标签转换为 h1 标签
if (currentNode.type === 'Element' && currentNode.tag === 'p') {
currentNode.tag = 'h1'
}
// 2. 文本节点的内容重复两次
if (currentNode.type === 'Text') {
currentNode.content = currentNode.content.repeat(2)
}
// 3. 递归遍历子节点
const children = currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
traverseNode(children[i])
}
}
}
function transform(ast) {
traverseNode(ast) // 调用 traverseNode 完成转换
console.log(dump(ast)) // 打印 AST 信息
}
问题:每加一个功能就要往
traverseNode里塞一段if,遍历与转换逻辑耦合,函数会越来越臃肿。
三、第二版:把转换逻辑拆成独立的转换函数
思路:traverseNode 只负责「遍历访问」,具体转换由注册进来的函数完成;这些函数放在转换上下文 context 的 nodeTransforms 数组里。
function traverseNode(ast, context) {
const currentNode = ast
// 遍历注册的转换函数,逐个执行
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
// 把当前节点 currentNode 和 context 都传进去
transforms[i](currentNode, context)
}
// 递归遍历子节点
const children = currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
traverseNode(children[i], context) // 透传 context
}
}
}
function transform(ast) {
// 在 transform 函数内创建 context 对象
const context = {
// 注册 nodeTransforms 数组
nodeTransforms: [
transformElement, // 转换标签节点
transformText // 转换文本节点
]
}
traverseNode(ast, context)
console.log(dump(ast))
function transformElement(node) {
if (node.type === 'Element' && node.tag === 'p') {
node.tag = 'h1'
}
}
function transformText(node) {
if (node.type === 'Text') {
node.content = node.content.repeat(2)
}
}
}
四、这样解耦的好处
- 节点操作与节点访问解耦:转换逻辑被封装在
transformElement、transformText这类独立函数中。 - 想加功能,只要再写一个转换函数并注册到
context.nodeTransforms即可,不用改traverseNode。 - 这就是编译器插件化架构的雏形:转换函数就是插件,遍历器只是执行插件的宿主。
易错点:递归调用
traverseNode时必须把context透传下去,否则子节点的转换函数拿到的是undefined。
相关
- 15.4 AST 的转换与插件化架构:AST 转换的整体定位
- 15.4.2 转换上下文与节点操作:
context里还要放哪些数据、如何替换/删除节点