vue3 compile系列二:transform

890 阅读2分钟

transform阶段根据不同的ast节点添加不同的选项参数,这些参数在generate阶段会用到。

大致分为这几个步骤:

1). 生成transform上下文

2). 转换结点

3). 静态提升相关操作

4). 赋值codegenNode

5). 对root结点添加meta信息

具体看下这几步 (packages/compiler-core/src/transform.ts)

生成transform上下文

createTransformContext定义中,options给了默认值。

返回的context是TransformContext类型的实例,包括了一些options,state和methods。 methods方法:

  • helper:context.helpers中添加helper
  • helperString:context.helpers中添加helper,并返回name在helperNameMap中的映射值
  • replaceNode:将node结点替换context.currentNode和ast中相应结点
  • removeNode:移除结点;如果不传参,则移除context.currentNode
  • onNodeRemoved:移除结点触发事件
  • addIdentifiers:非浏览器环境identifiers对应属性+1
  • removeIdentifiers:非浏览器环境identifiers对应属性-1
  • hoist:创建静态提升的identifier
  • cache:添加缓存

traverseNode遍历转换结点

function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  context.currentNode = node
  // apply transform plugins
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }

  // exit transforms
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}
  • context.currentNode暂存当前结点
  • 从context中取出nodeTransforms(getBaseTransformPreset返回属性与用户自定义参数的组合),定义返回值数组
  • 依次执行nodeTransforms中的transformOnce、transformIf、transformFor、transformElement、transformText、transformSlotoutlet等方法,并保存结果到exitFns中
  • 若node被移除,则退出循环;否则,还原暂存的结点,继续执行下一个transformXX指令
  • 循环退出之后,根据node.type的类型,执行后续操作,其中包括了子节点的递归和if的其他分支。
  • 最后,执行transformXX的回调

以transformIf为例,

createStructuralDirectiveTransform方法,转换v-if、v-for指令,并生成回调方法。 首先根据第一个参数name是string类型还是正则表达式,生成matches方法; 然后返回包含matches和第二个参数fn的闭包函数。这个函数就是transformIf方法。

const onExit = nodeTransforms[i](node, context)

transformIf --> processIf --> 返回exitFn,processIf的逻辑感兴趣的同学可以自己了解下。

hoistStatic

function hoistStatic(root: RootNode, context: TransformContext) {
  walk(
    root,
    context,
    new Map(),
    // Root node is unfortunately non-hoistable due to potential parent
    // fallthrough attributes.
    isSingleElementRoot(root, root.children[0])
  )
}
function isSingleElementRoot(
  root: RootNode,
  child: TemplateChildNode
): child is PlainElementNode | ComponentNode | TemplateNode {
  const { children } = root
  return (
    children.length === 1 &&
    child.type === NodeTypes.ELEMENT &&
    !isSlotOutlet(child)
  )
}

看一下walk对ast的遍历

  • 初始化hasHoistedNode、hasRuntimeConstant为false
  • 遍历结点的children,element类型和text call类型才可以被提升
    • element类型
      • 不是singleELementRoot,并且不是StaticType.NOT_STATIC(通过getStaticType获得)。如果是StaticType.HAS_RUNTIME_CONSTANT,hasRuntimeConstant置为true;用context.hoist设置codegenNode和codegenNode.patchFlag;hasHoistedNode置为true;继续下次循环,不需要子节点递归的过程;
      • 其他情况(结点可能存在动态子节点,但是可能有可以被静态提升的属性)。如果结点的codegenNode.type是NodeTypes.VNODE_CALL,flag(通过getPatchFlag(codegenNode)获得)不存在或者为PatchFlags.NEED_PATCH、PatchFlags.TEXT中的一个,存在可提升的静态属性,那么codegenNode.props就是context.hoist提升后的props
    • text call类型
      • getStaticType有静态内容,提升codegenNode,hasHoistedNode置为true;如果是StaticType.HAS_RUNTIME_CONSTANT,hasRuntimeConstant置为true;
    • 递归遍历(根据子节点类型)
      • element类型
      • v-for
      • v-if分支
  • 如果hasRuntimeConstant为false,hasHoistedNode为true,调用context.transformHoist(children, context, node);transformHoist在不同的平台会有各自的实现。

createRootCodegen

非ssr的情况下,调用createRootCodegen(root, context),对root的codegenNode属性赋值,codegenNode 是codegen阶段生成代码要用到的数据。

  • root只有一个子节点
    • 这个子节点是element,将其转换为block,将root.codegenNode赋值为child.codegenNode
    • 其他情况,例如单,if结点,for结点(已经是block了),单个text结点(已经patch过),直接将root.codegenNode赋值为child子节点
  • root有多个子节点,调用createVNodeCall转换为fragment block,如下图所示,最后呈现出来。
  • root没有子节点,直接返回null

添加meta信息

// finalize meta information
  root.helpers = [...context.helpers]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = [...context.imports]
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached