15.6 代码生成
代码生成本质是字符串拼接的艺术:按节点类型遍历 JavaScript AST,拼出渲染函数的源码。
一、思路
- 代码生成本质上是字符串拼接。
- 需要访问 JavaScript AST 中的节点,为每一种类型的节点生成相符的 JavaScript 代码。
- 与 AST 转换一样,代码生成也需要上下文对象,用来维护生成过程中程序的运行状态(已生成的代码、缩进级别)。
- 用
generate函数完成任务:扩展context增加换行/缩进工具函数,再写genNode按节点类型分发;以后新增节点类型,只需在genNode里加一个分支。
二、generate 与上下文
function generate(node) {
const context = {
// 存储最终生成的渲染函数代码
code: '',
// 通过 push 完成代码拼接
push(code) {
context.code += code
},
// 当前缩进级别,初始为 0
currentIndent: 0,
// 换行:追加 \n,并补上 currentIndent * 2 个空格保留缩进
newline() {
context.code += '\n' + ' '.repeat(context.currentIndent)
},
// 缩进:级别自增后换行
indent() {
context.currentIndent++
context.newline()
},
// 取消缩进:级别自减后换行
deIndent() {
context.currentIndent--
context.newline()
}
}
// 完成代码生成
genNode(node, context)
return context.code
}
三、genNode:按节点类型分发
function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl': genFunctionDecl(node, context); break
case 'ReturnStatement': genReturnStatement(node, context); break
case 'CallExpression': genCallExpression(node, context); break
case 'StringLiteral': genStringLiteral(node, context); break
case 'ArrayExpression': genArrayExpression(node, context); break
}
}
四、各类型节点的生成函数
// 函数声明
function genFunctionDecl(node, context) {
const { push, indent, deIndent } = context
push(`function ${node.id.name} `) // function render
push(`(`)
genNodeList(node.params, context) // 参数列表
push(`) `)
push(`{`)
indent() // 函数体缩进
node.body.forEach(n => genNode(n, context))
deIndent() // 取消缩进
push(`}`)
}
// 节点数组:用逗号分隔
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
genNode(nodes[i], context)
if (i < nodes.length - 1) push(`, `)
}
}
// 数组表达式
function genArrayExpression(node, context) {
const { push } = context
push(`[`)
genNodeList(node.elements, context)
push(`]`)
}
// return 语句
function genReturnStatement(node, context) {
const { push } = context
push(`return `)
genNode(node.return, context)
}
// 字符串字面量:要带上引号
function genStringLiteral(node, context) {
context.push(`'${node.value}'`)
}
// 函数调用
function genCallExpression(node, context) {
const { push } = context
const { callee, arguments: args } = node
push(`${callee.name}(`)
genNodeList(args, context)
push(`)`)
}
五、compile:串起三大阶段
function compile(template) {
const ast = parse(template) // 1. 模板 → 模板 AST
transform(ast) // 2. 模板 AST → JavaScript AST(结果在 ast.jsNode)
const code = generate(ast.jsNode) // 3. JavaScript AST → 渲染函数代码
return code
}
console.log(compile(`<div><p>Vue</p><p>Vue</p></div>`))
// function render () {
// return h('div', [h('p', 'Vue'), h('p', 'Vue')])
// }
六、小结
| 阶段 | 函数 | 输入 → 输出 |
|---|---|---|
| 解析 | parse | 模板字符串 → 模板 AST |
| 转换 | transform | 模板 AST → JavaScript AST(挂在 ast.jsNode) |
| 生成 | generate | JavaScript AST → 渲染函数字符串 |
易错点:
StringLiteral生成时要补上引号;indent()/deIndent()内部已经调用了newline(),不要再额外换行。 后续方向:第 16 章深入解析器(文本模式、递归下降、状态机启停、属性与插值等),第 17 章讲编译优化(静态提升、预字符串化等)。
相关
- 15.5 将模板 AST 转为 JavaScript AST:代码生成要处理的 JavaScript AST 从哪来