本节,将讨论如何将模板 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
}
]
}
}
}