15.4.3 进入与退出
有些转换必须等子节点全部处理完才能做,于是把节点的访问拆成**进入(enter)与退出(exit)**两个阶段。
一、顺序工作流的局限
- 转换节点时往往需要根据其子节点的情况决定如何转换当前节点,这就要求父节点的转换等所有子节点转换完毕后再执行。
- 之前的工作流是「从根节点开始、顺序执行」的:一个节点被处理时,它的父节点早就处理完了,无法再回过头重新处理父节点。
二、两阶段设计
- 转换函数可以返回另一个函数,返回的这个函数就是退出阶段的回调。
traverseNode把进入阶段收集到的退出回调存进exitFns数组,等子节点全部遍历完之后再执行。- 结果:
- 进入阶段:先进入父节点,再进入子节点;
- 退出阶段:先退出子节点,再退出父节点。
- 因此在退出阶段处理当前节点,就一定能够保证其子节点全部处理完毕。
function traverseNode(ast, context) {
context.currentNode = ast
// 1. 退出阶段的回调函数数组
const exitFns = []
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
// 2. 转换函数可以返回一个函数,该函数即作为退出阶段的回调函数
const onExit = transforms[i](context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
// 当前节点被移除 → 直接返回
if (!context.currentNode) return
}
// 递归遍历子节点
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
// 3. 最后阶段执行缓存下来的退出回调(注意要反序执行)
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
function transform(ast) {
const context = {
currentNode: null,
childIndex: 0,
parent: null,
replaceNode(node) { /* ... */ },
removeNode() { /* ... */ },
nodeTransforms: [testInAndOut, testInAndOut2]
}
traverseNode(ast, context)
console.log(dump(ast))
}
三、执行顺序
function testInAndOut(node, context) {
console.log('testInAndOut1 进入节点:', node.type + ':' + (node.tag ?? ''))
return function () {
console.log('testInAndOut1 退出节点:', node.type + ':' + (node.tag ?? ''))
}
}
// testInAndOut2 结构相同
| 维度 | 规律 |
|---|---|
| 进入阶段 | 先父后子;同一节点上按注册顺序执行(1 先于 2) |
| 退出阶段 | 先子后父;同一节点上按注册反序执行(2 的退出先于 1 的退出) |
易错点:退出回调是反序执行的——
testInAndOut比testInAndOut2先注册,它的进入阶段最先执行,但它的退出阶段最晚执行。
四、用途
- 第 15.5 节中
transformElement就把生成h(...)调用的逻辑写在退出阶段:只有子节点全部转换完、children[i].jsNode都就绪,才能组装出h(tag, children)。
相关
- 15.4.2 转换上下文与节点操作:
context上的数据与节点替换/删除 - 15.5 将模板 AST 转为 JavaScript AST:真正的转换插件都写在退出阶段