Skip to content

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 优化机制。

辅助函数收集

在转换过程中收集所需的运行时辅助函数,用于生成导入语句。