15.5 将模板 AST 转为 JavaScript AST
渲染函数是 JavaScript 代码,要生成它,就必须先把模板 AST 转成「描述这段 JavaScript 代码」的 JavaScript AST。
一、为什么要转
- 编译目标是渲染函数,而渲染函数由 JavaScript 代码描述。
- 与「模板 AST 是模板的描述」一样,JavaScript AST 是 JavaScript 代码的描述:本质上就是设计一组数据结构,来描述渲染函数的代码。
// 模板
`<div><p>Vue</p><p>Template</p></div>`
// 等价的渲染函数
function render() {
return h('div', [h('p', 'Vue'), h('p', 'Template')])
}
二、JavaScript AST 的节点类型
节点 type | 描述 | 关键属性 |
|---|---|---|
FunctionDecl | 函数声明语句 | id(标识符)、params(参数数组)、body(语句数组) |
ReturnStatement | return 语句 | return |
CallExpression | 函数调用表达式 | callee(被调用函数名)、arguments(参数数组) |
StringLiteral | 字符串字面量 | value |
ArrayExpression | 数组表达式 | elements |
Identifier | 标识符 | name |
// 对应 function render() {}
const FunctionDeclNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
// 函数体可以有多个语句,所以是数组
body: [{ type: 'ReturnStatement', return: null }]
}
// 对应 h()
const CallExp = {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: []
}
// 对应 'div' 与 []
const Str = { type: 'StringLiteral', value: 'div' }
const ArrayExp = { type: 'ArrayExpression', elements: [] }
用这些节点描述 function render() { return h('div', [h('p', 'Vue'), h('p', 'Template')]) }:
const FunctionDeclNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [
{
type: 'ReturnStatement',
return: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{ type: 'StringLiteral', value: 'div' },
{
type: 'ArrayExpression',
elements: [
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{ type: 'StringLiteral', value: 'p' },
{ type: 'StringLiteral', value: 'Vue' }
]
},
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{ type: 'StringLiteral', value: 'p' },
{ type: 'StringLiteral', value: 'Template' }
]
}
]
}
]
}
}
]
}
三、映射关系
| 模板 AST | JavaScript AST |
|---|---|
Root | FunctionDecl(名为 render 的函数声明) |
Element | CallExpression,callee 为 h |
Text | StringLiteral |
| 只有一个子节点 | 直接把子节点的 jsNode 作为 h 的第二个参数 |
| 有多个子节点 | 用 ArrayExpression 包起来,元素是各子节点的 jsNode |
转换产生的 JavaScript AST 节点都保存在模板 AST 节点的
node.jsNode属性下。
四、辅助函数
// 创建 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 }
}
五、转换实现
// 转换文本节点:文本 → 字符串字面量
function transformText(node) {
if (node.type !== 'Text') return
node.jsNode = createStringLiteral(node.content)
}
// 转换标签节点:标签 → h 函数调用
function transformElement(node) {
// 写在退出阶段的回调中,才能保证该标签节点的子节点全部处理完毕
return () => {
if (node.type !== 'Element') return
// 1. 创建 h 函数调用语句,第一个参数是标签名
const callExp = createCallExpression('h', [createStringLiteral(node.tag)])
// 2. 处理 h 函数调用的参数
node.children.length === 1
// 只有一个子节点 → 直接使用子节点的 jsNode
? callExp.arguments.push(node.children[0].jsNode)
// 多个子节点 → 创建 ArrayExpression 作为参数
: callExp.arguments.push(
createArrayExpression(node.children.map(c => c.jsNode))
)
// 3. 挂到 jsNode 属性下
node.jsNode = callExp
}
}
// 转换 Root 根节点:Root → render 函数声明
function transformRoot(node) {
// 同样写在退出阶段,保证子节点全部被处理完毕
return () => {
if (node.type !== 'Root') return
// 根节点的第一个子节点就是模板的根节点(暂不考虑多根节点)
const vnodeJSAST = node.children[0].jsNode
node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [{ type: 'ReturnStatement', return: vnodeJSAST }]
}
}
}
注册顺序:
nodeTransforms: [
transformText, // 转换文本节点
transformElement, // 转换标签节点
transformRoot // 转换 Root 根节点
]
const ast = parse(`<p>Vue</p>`)
transform(ast)
console.log(ast.jsNode) // FunctionDecl: render → return h('p', 'Vue')
为什么
transformElement/transformRoot要把逻辑写在退出阶段:只有子节点都转换完、children[i].jsNode都就绪,才能自底向上组装出h(tag, children),最终拼出render函数的返回值。 易错点:node.children.length === 1时直接传jsNode,多个子节点才包一层ArrayExpression——这就是「模板 AST 的children数组」到「h第二个参数」的映射规则。
相关
- 15.4.3 进入与退出:为什么转换逻辑要写在退出阶段
- 15.6 代码生成:把 JavaScript AST 拼成渲染函数源码