[Pattern] 前置与后置转换的处理
在转换AST节点的过程中,往往需要根据其子节点的情况来决定如何对当前节点进行转换,这就要求父节点的转换操作必须等待其所有子节点全部转换完毕之后再执行。
但是我们现在设计的转换流程并不支持这个能力,我们是从根节点开始,顺序往下执行的流程,如下图:

当一个节点被处理时,意味着它的子节点已经被处理完毕了,我们无法再回头重新处理父节点。
更加理想的转换流程应该像下面这样:

对节点的访问,其实可以分为两个阶段,进入和退出阶段,也可以称之为前置转换阶段(Pre-Transform)和后置转换阶段(Post-Transform)。
当转换函数处于进入阶段时,它会先进入父节点,再进入子节点。而当转换函数处于退出阶段时,会先退出子节点,再退出父节点。这样我们在退出节点阶段对当前访问的节点进行处理,就能保证子节点全部处理完毕。
比如下面的伪代码:
<MyComponent>
<template v-slot:header>Header Content</template>
<template v-slot:default>Default Content</template>
</MyComponent>
在这个例子中:
- 在进入阶段,会识别这是一个组件节点,并初始化一些基础信息。
- 但是,插槽内容需要等所有子节点(插槽模板)都处理完成后才能生成,这部分逻辑父节点就需要等待所有子节点全部处理完成才能执行。
为了能够处理这种情况,我们可以让转换函数返回一个回调函数,在traverseNode函数中增加一个数组,用来存储转换函数返回的回调函数,并且在traverseNode函数的最后,执行缓存在数组中的函数。这样就保证了,当退出阶段的回调函数执行时,当前访问节点的子节点已经全部被处理过了
有了这样的设计之后,我们在编写转换函数的时候,可以将转换逻辑卸载退出阶段的回调函数中,从而保证在对当前访问的节点进行转换之前,其子节点一定全部处理完毕了。
function transformElement(node) {
// console.log(`进入transformElement:${JSON.stringify(node)}`)
// return () => {
// console.log(`退出transformElement:${JSON.stringify(node)}`)
// }
}
function transformText(node, context) {
console.log(`进入transformText:${JSON.stringify(node)}`)
return () => {
console.log(`退出transformText:${JSON.stringify(node)}`)
}
}
traverseNode函数
function traverseNode(ast, context) {
// 设置当前转换的节点信息context.currentNode
context.currentNode = ast
// 增加退出阶段的回调函数数组
+ const exitFns = [];
// nodeTransforms是一个数组,每个元素都是一个函数
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
+ const onExit = transforms[i](context.currentNode, context)
+ if(onExit){
+ exitFns.push(onExit)
+ }
if(!context.currentNode){
// 如果当前节点为null,说明节点已经被删除,直接返回
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)
}
}
+ // 在节点处理的最后阶段,执行所有的退出函数
+ let i = exitFns.length
+ while(i--){
+ exitFns[i]()
+ }
}

浙公网安备 33010602011771号