【vue设计与实现】编译器 - 将模板 AST转为 JavaScript AST

本节,将讨论如何将模板 AST 转换为JavaScript AST,为后续讲解代码生成做铺垫。
将模板 AST转换为JavaScript AST是因为为了将模板编译成渲染函数。而谊染函数是由 JavaScript代码来描述的,因此,我们需要将模板 AST转换为用于描述渲染函数的JavaScript AST。

以之前的模板为例子

<div><p>Vue</p><p>Template</p></div>

与这段模板等价的渲染函数是:

function render(){
	return h('div'[
		h('p','Vue')h('p','Template')
	])
}

上面这段渲染函数的JavaScript代码所对应的JavaScript AST 就是我们的转换目标。

所以,需要设计一些数据结构来描述渲染函数的代码

首先,观察上面这段渲染函数的代码。它是一个函数声明,所以我们首先要描述JavaScript中的函数声明语句。一个函数声明语句由以下几部分组成:
id:函数名称
params:函数的参数
body: 函数体,由于函数体可以包含多个语句,因此它也是一个数组

为了简化问题,可以先设计一个基本的数据结构来描述函数声明语句

const FunctionDeclNode = {
	type: 'FunctionDecl',// 代表该节点是函数声明
	// 函数的名称是一个标识符,标识符本身也是一个节点
	id: {
		type: 'Identifier',
		name: 'render'
	},
	params: [],
	// 渲染函数的函数体只有一个语句,即 return 语句
	body: [
		{
			type: 'ReturnStatement',
			return null
		}
	]
}

每个节点都具有type字段,该字段用来代表节点的类型。

再来看一下渲染函数的返回值。渲染函数返回的是虚拟DOM节点,具体体现在h函数的调用。可以用CallExpression类型的节点来描述函数调用语句,如下面代码所示:

const CallExp = {
	type: 'CallExpression',
	// 被调用函数的名称,它是一个标识符
	callee: {
		type: 'Identifier',
		name: 'h'
	},
	//参数
	arguments: []
}

再次观察渲染函数的返回值

function render(){
	// h 函数的第一个参数是一个字符串字面量
	// h 函数的第二个参数是一个数组
	return h('div'[/*...*/])
}

可以看到,最外层的 h 函数的第一个参数是一个字符串字面量,可以使用StringLiteral的节点来描述它:

const Str = {
	type: 'StringLiteral',
	value: 'div'
}

最外层的h函数的第二个参数是一个数组,可以使用类型为ArrayExpression的节点描述它:

const Arr = {
	type: 'ArrayExpression',
	elements: []
}

使用上述 CallExpresstion、StringLiteral、 ArrayExpression 等节点来填充渲染函数的返回值,其最终结果如下面的代码所示:

const FunctionDeclNode = {
	type: 'FunctionDecl',
	id: {
		type: 'Identifier',
		name: 'render'
	},
	params: [],
	body: [
		{
			type: 'ReturnStatement',
			//最外层的 h 函数调用
			return: {
				type: 'CallExpression',
				callee: {type: 'Identifier', name: 'h'},	
				arguments: [
					// 第一个参数是字符串字面量,div'
					{
						type: 'StringLiteral',
						value: 'div'
					},
					// 第二个参数是一个数组
					{
						type: 'ArrayExpression',
						elements: [
						// 数组的第一个元素是 h 函数的调用
							{
								type:'CallExpression',
								callee: {
									type:'Identifier', 
									name: 'h'
								},	
								arguments:[								
									// 该 h 函数调用的第一个参数是字符串字面量
									{type:'StringLiteral',value: 'p'},
									{type:'StringLiteral',value: 'Vue'},
									
								]
							},
							
							{
								type:'CallExpression',
								callee: {
									type:'Identifier', 
									name: 'h'
								},	
								arguments:[
									{type:'StringLiteral',value: 'p'},
									{type:'StringLiteral',value: 'Template'},
								]
							},
						]
					}
				]
			
			}
		}
	]

}

如上面这段 JavaScript AST的代码所示,它是对渲染函数代码的完整描述。
接下来就要编写转换函数,将模板AST转换为上述JavaScript AST。不过在这之前,需要一些用来创建JavaScript AST节点的辅助函数

//用来创建StringLiteral 节点
function createStringLiteral(value){
	return {
		type: 'StringLiteral',
		value
	}
}

//用来创建Identifier节点
function createIdentifier(name){
	return {
		type: 'Identifier',
		name
	}
}

//用来创建ArrayExpression节点
function createArrayExpression(elements){
	return {
		type: 'ArrayExpression',
		elements
	}
}

//用来创建CallExpression节点
function createCallExpression(callee, arguments){
	return {
		type: 'CallExpression',
		callee: createIdentifier(callee),
		arguments
	}
}

有了这些辅助函数,可以更容易地编写转换代码

为了把模板AST转换为JavaScrip tAST,同样需要两个转换函数: transformElement 和 transformText,具体实现如下:

// 转换文本节点
function transformText(node){
	if(node.type !== 'Text'){
		return
	}
	//将文本节点对应的 Javacript AST 节点添加到 node.jsNode 属性下
	node.jsNode = createStringLiteral(node.content)
}
// 转换标签节点
function transformElement(node){
	//将转换代码编写在退出阶段的回调函数中
	//这样可以保证该标签节点的子节点全部被处理完毕
	return ()=>{
		if(node.type !== 'Element'){
			return
		}

		//1.创建 h 函数调用语句,
		//h函数调用的第一个参数是标签名称,因此我们以 node.tag 来创建一个字符串字面量节点
		//作为第一个参数
		const callExp = createCallExpression('h',[
			createStringLiteral(node.tag)
		])

		node.children.length === 1
		// 如果当前标签节点只有一个子节点,则直接使用子节点的 jsNode 作为参数
		? callExp.arguments.push(node.children[0].jsNode)
		//如果当前标签节点有多个子节点,则创建一个 ArrayExpression 节点作为参数
		: callExp.arguments.push(
			createArrayExpression(node.children.map(c => c.jsNode))
		)
		//3.将当前标签节点对应的 JavaScript AST 添加到 jsNode 属性下
		node.jsNode = callExp
	}
}

使用上面两个转换函数即可完成标签节点和文本节点的转换,即把模板转换成 h 函数的调用。但是,转换后得到的 AST 只是用来描述渲染函数 render 的返回值的,所以我们最后一步要做的就是,补全 JavaScript AST,即把用来描述 render 函数本身的函数声明语句节点附加到JavaScriptAST中。
这需要编写transformRoot函数来实现对Root根节点的转换:

// 转换Root根节点
function transformRoot(node){
	return ()=>{
		if(node.type!== 'Root'){
			return
		}
		//node 是根节点,根节点的第一个子节点就是模板的根节点
		//这里暂时不考虑模板存在多个根节点的情况
		const vnodeJSAST = node.children[0].jsNode
		// 创建render函数的声明语句节点,将 vnodeJSAST 作为 render函数体的返回语句
		node.jsNode = {
			type: 'FunctionDecl',
			id: {type: 'Identifier', name: 'render'},
			params: [],
			body: [
				{
					type: 'ReturnStatement',
					return:vnodeJSAST
				}
			]
		}
	}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值