Transform(代码转化)
TIP
代码转化是编译器的第二个阶段,将描述"模板长什么样"的 AST 转化为描述"代码应该怎么生成"的渲染树。
概述
代码转化的目的是把"语法树"转化成"渲染树":
- 语法树(AST):描述"模板长什么样"
- 渲染树(Codegen Tree):描述"代码应该怎么生成"
转化过程通过深度优先遍历 AST,为每个节点添加 codegenNode
属性,用于后续的代码生成。
核心流程
1. 创建转换上下文
typescript
function createTransformContext(root) {
const context = {
currentNode: root, // 当前处理的节点
parent: null, // 父节点
transformNode: [ // 转换函数数组
transformElement,
transformText,
transformExpression
],
helpers: new Map(), // 记录辅助函数使用次数
helper(name) { // 添加辅助函数
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
}
}
return context
}
2. 节点遍历策略
采用深度优先遍历,先处理子节点,再处理父节点:
typescript
function traverseNode(node, context) {
context.currentNode = node
const transforms = context.transformNode
const exits = []
// 收集所有转换函数的退出回调
for (let i = 0; i < transforms.length; i++) {
const exit = transforms[i](node, context)
exit && exits.push(exit)
}
// 递归处理子节点
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ELEMENT:
for (let i = 0; i < node.children.length; i++) {
context.parent = node
traverseNode(node.children[i], context)
}
break
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
break
}
// 执行退出回调(后序遍历)
let i = exits.length
while (i--) {
exits[i]()
}
}
三种核心转换
1. 元素转换 (transformElement)
将元素节点转换为 VNode 调用:
typescript
function transformElement(node, context) {
if (NodeTypes.ELEMENT === node.type) {
return function () { // 返回退出函数
const { tag, props, children } = node
// 处理属性
const properties = []
for (let i = 0; i < props.length; i++) {
properties.push({
key: props[i].name,
value: props[i].value
})
}
const propsExpression = properties.length > 0
? createObjectExpression(properties)
: null
// 处理子节点
let vnodeChildren = null
if (children.length === 1) {
vnodeChildren = children[0]
} else if (children.length > 1) {
vnodeChildren = children
}
// 创建 VNode 调用节点
node.codegenNode = createVnodeCall(
context,
tag,
propsExpression,
vnodeChildren
)
}
}
}
2. 文本转换 (transformText)
合并相邻的文本和插值节点,减少运行时开销:
typescript
function transformText(node, context) {
if (NodeTypes.ELEMENT === node.type || node.type === NodeTypes.ROOT) {
return function () {
const children = node.children
let container = null
// 合并相邻的文本节点
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child)) { // 文本或插值
for (let j = i + 1; j < children.length; j++) {
const next = children[j]
if (isText(next)) {
// 创建复合表达式
if (!container) {
container = children[i] = {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [child]
}
}
container.children.push('+', next)
children.splice(j, 1)
j-- // 删除后需要回退
} else {
container = null
break
}
}
}
}
// 为文本节点创建 createTextVNode 调用
if (hasText && children.length > 1) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
children[i] = {
type: NodeTypes.TEXT_CALL,
content: child,
codegenNode: createCallExpression(context, [child])
}
}
}
}
}
}
}
3. 表达式转换 (transformExpression)
为插值表达式添加上下文前缀:
typescript
function transformExpression(node, context) {
if (NodeTypes.INTERPOLATION === node.type) {
// {{ msg }} -> {{ _ctx.msg }}
node.content.content = `_ctx.${node.content.content}`
}
}
根节点处理
处理根节点的特殊情况,决定是否使用 Block 优化:
typescript
function createRootCodegenNode(ast, context) {
const { children } = ast
if (children.length === 1) {
const child = children[0]
if (child.type === NodeTypes.ELEMENT) {
// 单个元素根节点,使用 Block 优化
ast.codegenNode = child.codegenNode
context.removeHelper(CREATE_ELEMENT_VNODE)
context.helper(CREATE_ELEMENT_BLOCK)
context.helper(OPEN_BLOCK)
ast.codegenNode.isBlock = true
} else {
ast.codegenNode = child
}
} else if (children.length > 1) {
// 多个根节点,使用 Fragment
ast.codegenNode = createVnodeCall(
context,
context.helper(Fragment),
undefined,
children
)
context.helper(CREATE_ELEMENT_BLOCK)
context.helper(OPEN_BLOCK)
}
}
转换示例
以模板 <div>Hello !</div>
为例:
转换前(AST)
javascript
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
children: [
{ type: NodeTypes.TEXT, content: 'Hello ' },
{ type: NodeTypes.INTERPOLATION, content: { content: 'name' } },
{ type: NodeTypes.TEXT, content: '!' }
]
}]
}
转换后(Codegen Tree)
javascript
{
type: NodeTypes.ROOT,
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: 'div',
children: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: ['Hello ', '+', '_ctx.name', '+', '!']
},
isBlock: true
},
helpers: ['openBlock', 'createElementBlock', 'toDisplayString']
}
关键概念
退出函数模式
转换函数返回一个退出函数,在子节点处理完成后执行,确保父节点能获取到子节点的完整转换结果。
Block 优化
对于单个元素根节点,使用 createElementBlock
代替 createElementVNode
,启用 Vue 的 Block 优化机制。
辅助函数收集
在转换过程中收集所需的运行时辅助函数,用于生成导入语句。