Vue3源码解读(5)-transform与generate

1,836 阅读3分钟

上篇文章中我们介绍了 Vue3 将 template 解析成 ast 的过程,得到 ast 后,我们就可以对 ast 上的节点和属性进行相应的操作,也就是之前我们看过的 transform 的过程。

1.transform

  // 遍历 AST 节点树,对上面生成的 AST 进行指令转换,生成可用节点,同时根据 compiler
  // 传入的配置(如是否做静态节点提升等)对 AST 节点树进行优化处理,为 rootNode 及
  // 下属每个节点挂载 codegenNode
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

我们来详细看一下 transform 方法。

/**
{
  type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标识
  loc, // 位置信息
  isConstant, // 是否是常量
  content, // 表达式内容
  // 是否是静态的,
  // e.g. v-bind:attr="value",value如果是动态变化的变量
  // v-bind:attr="true",true是常量不会变化,因此是静态的
  isStatic 
}
// 比如v-bind:attr="true",true转换为简单表达式对象就是
// { isContant: true, content: 'true', isStatic: true ... }
 */

// transform 函数对静态提升其决定性作用的两件事:
// 1. 将原始 AST 中的静态节点对应的 AST Element 赋值给根 AST 的 hoists 属性。
// 2. 获取原始 AST 需要的 helpers 对应的键名,用于 generate 阶段的生成可执行代码的获取对应函数,
//    例如 createTextVNode、createStaticVNode、renderList 等等。

// 并且,在 traverseNode 函数中会对 AST Element 应用具体的 transform 函数,大致可以分为两类:
// 1. 静态节点 transform 应用,即节点不含有插值、指令、props、动态样式的绑定等。
// 2. 动态节点 transform 应用,即节点含有插值、指令、props、动态样式的绑定等。

// `<div>hi vue3</div>` 会命中 transformElement 和 transformText 两个 plugin 的逻辑。
export function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}

其中最核心的步骤是 traverseNode :

// 遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,
// 子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,
// 都是由nodeTransforms中对应的指令转换工具完成的。
// 经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),
// 该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。
// 本质上codegenNode是一个表达式对象。
export 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++) {
    // 依次执行 nodeTransforms
    // Transform 会返回一个退出函数,在处理完所有的子节点后再执行
    // 其中的包含我们调用 compile 的时候传入的 options.nodeTransforms
    // 转换器:transformElement、transformExpression、transformText、 
    // transformElement负责整个节点层面的转换,
    // transformExpression负责节点中表达式的转化,
    // transformText负责节点中文本的转换,转换后会增加一堆表达式表述对象
    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:
      // 处理 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
  // 执行所有 Transform 的退出函数
  while (i--) {
    exitFns[i]()
  }
}

在上一篇模板编译里面我们曾讲过,默认的 nodeTransforms 包括目标节点结构的转换插件、指令转换插件两大类, 我们可以看一下之前提到的这两类中的 transformIf 插件。

export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    /**
    在 transformIf 方法体内我们可以通过操作 node 来实现一些自定义的节点、属性的转换规则
    nodeTransform 方法的返回值就是退出函数,退出函数会在退出 traverseNode 方法前被依次执行
    while (i--) {
      exitFns[i]()
    }
     */
    // processIf 会在退出 traverseNode 方法前被执行
    // 进行 if 节点的处理,为其标记相应的 codegenNode
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // #1587: We need to dynamically increment the key based on the current
      // node's sibling nodes, since chained v-if/else branches are
      // rendered at the same depth
      const siblings = context.parent!.children
      let i = siblings.indexOf(ifNode)
      let key = 0
      while (i-- >= 0) {
        const sibling = siblings[i]
        if (sibling && sibling.type === NodeTypes.IF) {
          key += sibling.branches.length
        }
      }

      // Exit callback. Complete the codegenNode when all children have been
      // transformed.
      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // attach this branch's codegen node to the v-if root.
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)

我们参考 transformIf 方法,就可以实现自定义的 transform 方法,来对 Vnode 和 attrs 进行一些特殊操作了。

看完 transform ,接下来就是 generate 阶段了。generate 阶段的主要过程就是将 transform 转换后的 AST 生成对应的可执行代码,从而在之后 Runtime 的 Render 阶段时,

就可以通过可执行代码生成对应的 VNode Tree,然后最终在页面上映射为真实的 DOM Tree 。

TODO: export function generate

TODO:走完亲戚继续写

Vue3 源码解读

参考链接:

blog.csdn.net/u014125106/…