【vue设计与实现】编译器 - 转换上下文与节点操作 & 进入与退出

本文介绍了一种抽象语法树(AST)转换流程,包括构建转换上下文、节点替换、节点移除及实现双阶段转换等功能。文章详细解释了如何通过递归遍历AST并对节点进行转换,确保在对当前节点进行转换前,其所有子节点已被处理。
摘要由CSDN通过智能技术生成

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 () => {
		// 在这里编写退出节点的逻辑,当这里的代码运行时,当前转换节点的子节点一定处理完毕了
	}

}

另外还有一点需要注意,退出阶段的回调函数是反序执行的。这意味着,如果注册了多个转换函数,则它们的注册顺序将决定代码的执行结果。
进入阶段是顺序执行,退出阶段是倒序执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值