context.nodeTransforn 数组,这里的 context 可以看作AST转换函数过程中的上下文数据。所有 AST转换函数都可以通过 context来共享数据。上下文对象中通常会推护程序的当前状态。上下文对象中包含的信息对于编写复杂的转换函数非常有用。接下来我们要做的就是构造转换上下文信息,如下面代码:
function transform(ast){
const context = {
// 增加 currentNode,用来存储当前正在转换的节点
currentNode: null,
//增加 childIndex,用来存储当前节点在父节点的 children 中的位置索引
childIndex: 0,
// 增加 parent,用来存储当前转换节点的父节点
parent: null,
nodeTransforms:[
transformElement,
transformText
]
}
traverseNode(ast, context)
console.log(dump(ast))
}
下面,需要在合适的地方设置转换上下文对参中的数据,如下面traverseNode函数的代码所示:
// 接收第二个参数context
function traverseNode(ast, context){
// 设置当前转换的节点信息 context.currentNode
context.currentNode = ast
const transforms = context.nodeTransforms
for(let i = 0;i<transforms.length;i++){
// 将当前节点 currentNode 和 context 都传递给 nodeTransforms 中注册的回调函数
transforms[i](context.currentNode,context)
}
const children = context.currentNode.children
if(children){
for(let i=0;i<children.length;i++){
//递归地调用 traverseNode 转换子节点之前,将当前节点设置为父节点
context.parent = context.currentNode
//设置位置索引
context.childIndex = i
//递归地调用时,将 context 传过来
traverseNode(children[i],context)
}
}
}
上面代码的关键点在于,在递归地调用 traverseNode 函数进行子节点的转换之前,必须设置 context.parent 和 context.childIndex 的值,这样才能保证在接下来的递归节点替换转换中,context对象所存储的信息是正确的。
有了上下文数据后,我们就可以实现节点替换功能了。节点替换是在对AST进行转换的时候,把某些节点替换为其他类型的节点。为了完成节点替换,需要在上下文对象中添加context.replaceNode 函数。该函数接收新的AST节点作为参数,并使用新节点替换当前正在转换的节点,如下面的代码所示:
function transform(ast){
const context = {
currentNode: null,
parent: null,
// 用于替换节点的函数,接收新节点作为参数
replaceNode(node){
// 为了替换节点,我们需要修改 AST
// 找到当前节点在父节点的 children 中的位置:context.childIndex
// 然后使用新节点替换即可
context.parent.children[context,childIndex] = node
// 由于当前节点已经被新节点替换掉了,因此我们需要将 currentNode 更新为新节点
context.currentNode = node
}
nodeTransforms:[
transformElement,
transformText
]
}
traverseNode(ast, context)
console.log(dump(ast))
}
接下来,我们就可以在转换函数中使用 replaceNode函数对 AST中的节点进行。如下面transformText 函数的代码所示,它能够将文本节点转换为元素节点
//转换函数的第二个参数就是 context 对象
function transformText(node,context){
if(node.type === 'Text'){
//当前转换的节点是文本节点,则调用 context.replaceNode 函数将其替接为元素节点
context.replaceNode({
type: 'Element',
tag: 'span'
})
}
}
除了替换节点,有时还希望移除当前访问的节点。可以通过实现 context.removeNode函数来达到目的,如下面代码:
function transform(ast){
const context = {
currentNode: null,
parent: null,
replaceNode(node){
context.parent.children[context,childIndex] = node
context.currentNode = node
},
removeNode(){
if(context.parent){
// 调用数组的 splice 方法,根据当前节点的索引删除当前节点
context.parent.children.splice(context.childIndex,1)
//将context;currentNode置空
context.currentNode = null
}
}
nodeTransforms:[
transformElement,
transformText
]
}
traverseNode(ast, context)
console.log(dump(ast))
}
这里有一点需要注意,由于当前节点被移除了,所以后续的转换函数将不再需要处理该节点。因此,我们需要对 traverseNode 函数做一些调整,如下面的代码所示:
// 接收第二个参数context
function traverseNode(ast, context){
context.currentNode = ast
const transforms = context.nodeTransforms
for(let i = 0;i<transforms.length;i++){
transforms[i](context.currentNode,context)
// 由于任何转换函数都可能移除当前节点,因此每个转换函数执行完毕后
//都应该检查当前节点是否已经被移除,如果被移除了,直接返回即可
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)
}
}
}
如果节点被某个转换函数移除了,则traverseNode直接返回即可,无须做后续的处理
有了context.removeNode 函数之后,即可实现用于移除文本节息的转换函数.如代码所示:
function transformText(node,context){
if(node.type === 'Text'){
context.removeNode()
}
}
在转换 AST节点的过程中,往往需要根据其子节点的情况来决定如何对当前节点进行转换,这就要求父节点的转换操作必须等待其所有子节点杂点转换究毕后再执行。
现在的转换工作流并不支持这一能力,因为现在的是一种从根节点开始,顺序执行的工作流
这种顺序处理的工作流存在的问题是,当一个节点被处理时,意味着它的父节点已经被处理完毕,并且无法再回过头重新处理父节点。
理想的顺序应该是:对节点的访问分为两个阶段,即进入阶段和退出阶段。当转换函数处进入阶段时,它会先进人父节点,再进人子节点。而当转换函数处于退出阶段时,则会先退出子节点,再退出父节点。这样,只要在退出阶段对当前访问的节点进行处理,就一定能够证其子节点全部处理完毕。
为了实现这个转换工作流,需要重新设计转换函数的能力
如下面traverseNode函数的代码:
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 数组中
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)
}
}
//在节点处理的最后阶段执行缓存到 exitFns 中的回调函数
//注意,这里我们要反序执行
let i = exitFns.length
while(i--){
exitFns[i]()
}
}
这样就保证了当退出阶段的回调函数执行时,当前访问的节点的子节点已经全部处理过了。
因此在编写转换函数时,可以将转换逻辑编写在退出阶段的回调函数中,从而保证在对当前访问的节点进行转换之前,其子节点一定全部处理完毕了,如下面的代码所示:
function transformElement(node, context){
// 进入节点
// 返回一个会在退出节点时执行的回调函数
return () => {
// 在这里编写退出节点的逻辑,当这里的代码运行时,当前转换节点的子节点一定处理完毕了
}
}
另外还有一点需要注意,退出阶段的回调函数是反序执行的。这意味着,如果注册了多个转换函数,则它们的注册顺序将决定代码的执行结果。
进入阶段是顺序执行,退出阶段是倒序执行