编译器__Transform

226 阅读7分钟

在了解了编译器的parse阶段后,继续学习编译过程中的转换(Tranform)阶段 本文以模板源码如下为例展开对应的知识学习:

<div id="app">
  <button @click="insert">insert at random index</button>
  <span>{{msg}} 1111</span>
<span class="static">{{name + msg}}</span>
  <p>普通静态文本</p >
  <ul>
    <li v-for="item in  items" :key="item">{{item}}</li>
  </ul>
  <span v-if="show">showsshow</span>
  <span v-else>show==false</span>
</div>
</div>

通过parse阶段创建AST的结果内容为记录了节点的标签名,内容,类型,属性,闭合状态,修饰符 以及节点在源 HTML 字符串中的位置,包含行,列,偏移量等信息....大致树结构如下图所示:

image.png

transform

AST对象是对模板的完整描述,但是它还不能直接拿来生成代码,因为它的语义化还不够,没有包含和编译优化的相关属性,在 transform 阶段,Vue 会对 AST 进行一些转换操作,主要是根据不同的 AST 节点添加不同的编译优化选项参数先通过 getBaseTransformPreset 方法获取节点和指令转换的内置方法,然后调用 transform 方法做 AST 转换,并且把这些节点和指令的转换方法作为配置的属性参数传入

function getBaseTransformPreset(prefixIdentifiers) {

  return [

    [

      transformOnce,

      transformIf,

      transformFor,

      transformExpression,

      transformSlotOutlet,

      transformElement,

      trackSlotScopes,

      transformText

    ],

    {

      on: transformOn,

      bind: transformBind,

      model: transformModel

    }

  ]

}

创建 transform 上下文

首先,我们来看创建 transform 上下文的过程,其实和 parse 过程一样,在 transform 阶段会创建一个上下文对象,它的实现的结果: image.png

这个上下文对象 context 维护了 transform 过程的一些配置,比如前面提到的节点和指令的转换函数等;还维护了 transform 过程的一些状态数据,比如当前处理的 AST 节点,当前 AST 节点在子节点中的索引,以及当前 AST 节点的父节点等。此外,context 还包含了在转换过程中可能会调用的一些辅助函数,和一些修改 context 对象的方法。

遍历 AST 节点

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]()
  }
}

几个内置转换方法解析
transformElement() 元素节点转换

主体代码

判断节点是不是一个 Block 节点

为了运行时的更新优化,Vue.js 3.0 设计了一个 Block tree 的概念。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,极大优化了 diff 的效率,模板的动静比越大,这个优化就会越明显。 因此在编译阶段,我们需要找出哪些节点可以构成一个 Block,其中动态组件、svg、foreignObject 标签以及动态绑定的 prop 的节点都被视作一个 Block。

    // 动态组件、svg、foreignObject 标签以及动态绑定 key prop 的节点都被视作一个 Block
    let shouldUseBlock =
      // dynamic component may resolve to plain elements
      isDynamicComponent ||
      vnodeTag === TELEPORT ||
      vnodeTag === SUSPENSE ||
      (!isComponent &&
        // <svg> and <foreignObject> must be forced into blocks so that block
        // updates inside get proper isSVG flag at runtime. (#639, #643)
        // This is technically web-specific, but splitting the logic out of core
        // leads to too much unnecessary complexity.
        (tag === 'svg' || tag === 'foreignObject'))
    // 把 KeepAlive 看做是一个 Block,这样可以避免它的子节点的动态节点被父 Block 收集
      if (vnodeTag === KEEP_ALIVE) {
        // Although a built-in component, we compile KeepAlive with raw children
        // instead of slot functions so that it can be used inside Transition
        // or other Transition-wrapping HOCs.
        // To ensure correct updates with block optimizations, we need to:
        // 1. Force keep-alive into a block. This avoids its children being
        //    collected by a parent block.
        shouldUseBlock = true
        // 2. Force keep-alive to always be updated, since it uses raw children.
        // 2. 确保它始终更新
        patchFlag |= PatchFlags.DYNAMIC_SLOTS
        if (__DEV__ && node.children.length > 1) {
          context.onError(
            createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
              start: node.children[0].loc.start,
              end: node.children[node.children.length - 1].loc.end,
              source: ''
            })
          )
        }
      

处理节点的 props

这个过程主要是从 AST 节点的 props对象中进一步解析出指令 vnodeDirectives、动态属性 dynamicPropNames,以及更新标识 patchFlag。patchFlag 主要用于标识节点更新的类型,在组件更新的优化中会用到。

    // 处理 props
    if (props.length > 0) {
      const propsBuildResult = buildProps(
        node,
        context,
        undefined,
        isComponent,
        isDynamicComponent
      )
      vnodeProps = propsBuildResult.props
      patchFlag = propsBuildResult.patchFlag
      dynamicPropNames = propsBuildResult.dynamicPropNames
      const directives = propsBuildResult.directives
      vnodeDirectives =
        directives && directives.length
          ? (createArrayExpression(
              directives.map(dir => buildDirectiveArgs(dir, context))
            ) as DirectiveArguments)
          : undefined

      if (propsBuildResult.shouldUseBlock) {
        shouldUseBlock = true
      }
    }

处理节点的 children

    // 处理 children
    if (node.children.length > 0) {
      // 把 KeepAlive 看做是一个 Block,这样可以避免它的子节点的动态节点被父 Block 收集
      if (vnodeTag === KEEP_ALIVE) {
        // Although a built-in component, we compile KeepAlive with raw children
        // instead of slot functions so that it can be used inside Transition
        // or other Transition-wrapping HOCs.
        // To ensure correct updates with block optimizations, we need to:
        // 1. Force keep-alive into a block. This avoids its children being
        //    collected by a parent block.
        shouldUseBlock = true
        // 2. Force keep-alive to always be updated, since it uses raw children.
        // 2. 确保它始终更新
        patchFlag |= PatchFlags.DYNAMIC_SLOTS
        if (__DEV__ && node.children.length > 1) {
          context.onError(
            createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
              start: node.children[0].loc.start,
              end: node.children[node.children.length - 1].loc.end,
              source: ''
            })
          )
        }
      }

      const shouldBuildAsSlots =
        isComponent &&
        // / Teleport不是一个真正的组件,它有专门的运行时处理
        vnodeTag !== TELEPORT &&
        // explained above.
        vnodeTag !== KEEP_ALIVE

      if (shouldBuildAsSlots) {
        // 组件有 children,则处理插槽
        const { slots, hasDynamicSlots } = buildSlots(node, context)
        vnodeChildren = slots
        if (hasDynamicSlots) {
          patchFlag |= PatchFlags.DYNAMIC_SLOTS
        }
      } else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
        const child = node.children[0]
        const type = child.type
        // check for dynamic text children
        const hasDynamicTextChild =
          type === NodeTypes.INTERPOLATION ||
          type === NodeTypes.COMPOUND_EXPRESSION
        // 如果只是一个普通文本节点、插值或者表达式,直接把节点赋值给 vnodeChildren
        if (
          hasDynamicTextChild &&
          getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
        ) {
          patchFlag |= PatchFlags.TEXT
        }
        // pass directly if the only child is a text node
        // (plain / interpolation / expression)
        if (hasDynamicTextChild || type === NodeTypes.TEXT) {
          vnodeChildren = child as TemplateTextChildNode
        } else {
          vnodeChildren = node.children
        }
      } else {
        vnodeChildren = node.children
      }
    }

对于一个组件节点而言,如果它有子节点,则说明是组件的插槽,另外还会有对一些内置组件比如 KeepAlive、Teleport 的处理逻辑。

对于一个普通元素节点,我们通常直接拿节点的 children 属性给 vnodeChildren 即可,但有一种特殊情况,如果节点只有一个子节点,并且是一个普通文本节点、插值或者表达式,那么直接把节点赋值给 vnodeChildren。

处理 patchFlag 和 dynamicPropNames

对props解析结果patchFlag 和 dynamicPropNames 做进一步处理

    // 处理 patchFlag 和 dynamicPropNames
    if (patchFlag !== 0) {
      if (__DEV__) {
        if (patchFlag < 0) {
          // special flags (negative and mutually exclusive)
          vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
        } else {
          // 获取 flag 对应的名字,生成注释,方便理解生成代码对应节点的 pathFlag
          const flagNames = Object.keys(PatchFlagNames)
            .map(Number)
            .filter(n => n > 0 && patchFlag & n)
            .map(n => PatchFlagNames[n])
            .join(`, `)
          vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
        }
      } else {
        vnodePatchFlag = String(patchFlag)
      }
      if (dynamicPropNames && dynamicPropNames.length) {
        vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
      }
    }

通过过 createVNodeCall 创建了实现 VNodeCall 接口的代码生成节点

codegenNode 相比之前的 AST 节点对象,多了很多和编译优化相关的属性,它们会在代码生成阶段会起到非常重要作用

  node.codegenNode = createVNodeCall(
      context,
      vnodeTag,
      vnodeProps,
      vnodeChildren,
      vnodePatchFlag,
      vnodeDynamicProps,
      vnodeDirectives,
      !!shouldUseBlock,
      false /* disableTracking */,
      isComponent,
      node.loc
    )
export function createVNodeCall(
  context: TransformContext | null,
  tag: VNodeCall['tag'],
  props?: VNodeCall['props'],
  children?: VNodeCall['children'],
  patchFlag?: VNodeCall['patchFlag'],
  dynamicProps?: VNodeCall['dynamicProps'],
  directives?: VNodeCall['directives'],
  isBlock: VNodeCall['isBlock'] = false,
  disableTracking: VNodeCall['disableTracking'] = false,
  isComponent: VNodeCall['isComponent'] = false,
  loc = locStub
): VNodeCall {
  if (context) {
    if (isBlock) {
      context.helper(OPEN_BLOCK)
      context.helper(getVNodeBlockHelper(context.inSSR, isComponent))
    } else {
      context.helper(getVNodeHelper(context.inSSR, isComponent))
    }
    if (directives) {
      context.helper(WITH_DIRECTIVES)
    }
  }

  return {
    type: NodeTypes.VNODE_CALL,
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock,
    disableTracking,
    isComponent,
    loc
  }
}

它最后返回了一个对象,包含了传入的参数数据。这里要注意 context.helper 函数的调用,它会把一些 Symbol 对象添加到 context.helpers 数组中,目的是为了后续代码生成阶段,生成一些辅助代码

表达式转换transformExpression()

transformExpression对插值表达式,元素指令动态表达式,插槽插值表达式,过滤掉v-on 和 v-for ,因为它们都有各自的处理逻辑

举个例子,比如这个模板:{{ name + msg }} 经过 processExpression 处理后,node.content 的值变成了一个复合表达式对象:

"version": "3.2.40"结果依旧为{{name + msg}}

image.png

transformText()文本转换

transformText 函数只处理根节点、元素节点、 v-for 以及 v-if 分支相关的节点,它也会返回一个退出函数,因为 transformText 要保证所有表达式节点都已经被处理才执行转换逻辑。

transformText 主要的目的就是合并一些相邻的文本节点,然后为内部每一个文本节点创建一个代码生成节点。 <span>{{msg}} 1111</span>两个文本节点 合并成一个复合表达式节点

image.png

transformIf()if判断转换

遍历过程中遇见v-if 代码块的时候创建 IF 节点分支给主分支生成 codegenNode;后续继续遇到条件语句把元素节点移至IF分支中将条件分支的 codegenNode 附加到 上一个条件节点的 codegenNode 的 alternate 中。

      // 退出回调函数,当所有子节点转换完成执行
      return () => {
        // v-if 节点的退出函数
        // 创建 IF 节点的 codegenNode
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // v-else-if、v-else 节点的退出函数
          // 若出现其他条件分支
          // 将此分支的 codegenNode 附加到 上一个条件节点的 codegenNode 的 alternate 中
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }

v-if的转换结果如图:

image.png

image.png

transformFor() 方法 转换v-for

v-for方法转换后的children 有个parseResult里面存放对应的‘源’和‘值关键字’

image.png

hoistStatic静态提升

hoistStatic 主要就是从根节点开始,通过递归的方式去遍历节点,只有普通元素和文本节点才能被静态提升,所以针对这些节点,这里通过 getStaticType(getConstantType) 去获取静态类型,如果节点是一个元素类型,getStaticType 内部还会递归判断它的子节点的静态类型。

虽然有的节点包含一些动态子节点,但它本身的静态属性还是可以被静态提升的。

 child.codegenNode = context.hoist(child.codegenNode)
改动后的 codegenNode 会在生成代码阶段帮助我们生成静态提升的相关代码

上述例子的静态提升的内容如下:

1665315850399.png

createRootCodegen 生成根节点

完成静态提升后,我们来到了 AST 转换的最后一步,即创建根节点的代码生成节点

function createRootCodegen(root, context) {

  const { helper } = context;

  const { children } = root;

  const child = children[0];

  if (children.length === 1) {

    // 如果子节点是单个元素节点,则将其转换成一个 block

    if (isSingleElementRoot(root, child) && child.codegenNode) {

      const codegenNode = child.codegenNode;

      if (codegenNode.type === 13 /* VNODE_CALL */) {

        codegenNode.isBlock = true;

        helper(OPEN_BLOCK);

        helper(CREATE_BLOCK);

      }

      root.codegenNode = codegenNode;

    }

    else {

      root.codegenNode = child;

    }

  }

  else if (children.length > 1) {

    // 如果子节点是多个节点,则返回一个 fragement 的代码生成节点

    root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true);

  }

}

createRootCodegen 为 root 这个虚拟的 AST 根节点创建一个代码生成节点,如果 root 的子节点 children 是单个元素节点,则将其转换成一个 Block,把这个 child 的 codegenNode 赋值给 root 的 codegenNode。

如果 root 的子节点 children 是多个节点,则返回一个 fragement 的代码生成节点,并赋值给 root 的 codegenNode。

至此 创建 codegenNode 就是为了后续生成代码时使用

createRootCodegen 完成之后,接着把 transform 上下文在转换 AST 节点过程中创建的一些变量赋值给 root 节点对应的属性,在这里可以看一下这些属性

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

1665316209432(1).png