阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 转换(Transform)阶段的工作

转换(Transform)阶段的工作

作者:陈川 阅读数:3312人阅读 分类: Vue.js

转换(Transform)阶段的工作

Vue3的编译过程分为三个主要阶段:解析(Parse)、转换(Transform)和生成(Codegen)。转换阶段位于解析之后,生成之前,负责对解析得到的AST进行各种处理和优化。这个阶段是Vue模板编译过程中最复杂的部分,包含了众多重要的功能实现。

AST节点转换

转换阶段首先会对解析阶段生成的原始AST进行节点转换。Vue内部通过transform函数处理AST,这个函数会递归遍历AST树,对每个节点应用相应的转换函数:

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

每个节点转换函数都会接收当前节点和转换上下文作为参数。例如,对于元素节点的转换:

const transformElement = (node, context) => {
  // 处理props
  const props = node.props
  const vnodeTag = `"${node.tag}"`
  const vnodeProps = buildProps(node, context)
  
  // 处理子节点
  const children = node.children
  let vnodeChildren
  
  if (children.length) {
    vnodeChildren = children[0]
  }
  
  // 创建代码生成节点
  node.codegenNode = createVNodeCall(
    context,
    vnodeTag,
    vnodeProps,
    vnodeChildren
  )
}

静态提升(Hoist Static)

Vue3的一个重要优化是静态提升,即在编译阶段识别出静态内容并将其提升到渲染函数之外。这样可以避免在每次重新渲染时重新创建这些静态节点:

function hoistStatic(root: RootNode, context: TransformContext) {
  walk(root, context, 
    (node, context) => {
      if (node.type === NodeTypes.ELEMENT && isStaticNode(node)) {
        node.static = true
        context.hoists.push(node)
        return true
      }
    }
  )
}

例如,对于模板<div><span>static</span><span>{{ dynamic }}</span></div>,第一个<span>会被提升:

const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "static")

function render(_ctx) {
  return _createVNode("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ])
}

补丁标志(Patch Flags)

转换阶段还会为动态节点添加补丁标志,帮助运行时更高效地更新DOM。这些标志指示了节点哪些部分需要被比较和更新:

function markDynamicProps(node: ElementNode) {
  const flagNames = []
  if (hasDynamicKeys(node.props)) {
    flagNames.push('PROPS')
  }
  if (node.children.some(isDynamic)) {
    flagNames.push('CHILDREN')
  }
  if (flagNames.length) {
    node.patchFlag = getPatchFlag(flagNames)
  }
}

例如,一个只有class动态的元素会被标记为8PROPS标志),表示只需要比较props:

_createVNode("div", { class: _normalizeClass(_ctx.className) }, null, 8 /* PROPS */, ["class"])

缓存事件处理函数

为了避免不必要的重新渲染,Vue3会对事件处理函数进行缓存。转换阶段会识别出事件处理程序并添加缓存标志:

function transformExpression(node: Node, context: TransformContext) {
  if (node.type === NodeTypes.ATTRIBUTE && node.name === 'on') {
    const exp = node.value
    if (exp.content.startsWith('_cache') || exp.isHandlerKey) {
      return
    }
    node.value = {
      type: NodeTypes.SIMPLE_EXPRESSION,
      content: `_cache[${context.cacheIndex++}] || (_cache[${context.cacheIndex++}] = ${exp.content})`,
      isStatic: false,
      isConstant: false
    }
  }
}

转换后的代码会像这样:

_createVNode("button", { onClick: _cache[0] || (_cache[0] = $event => _ctx.handleClick($event)) }, "click me")

块(Block)处理

Vue3引入了块的概念,将模板划分为动态和静态区域。转换阶段会识别这些块并做相应标记:

function createStructuralDirectiveTransform(
  name: string | RegExp,
  fn: StructuralDirectiveTransform
): NodeTransform {
  return (node, context) => {
    if (node.type === NodeTypes.ELEMENT) {
      const { props } = node
      for (let i = 0; i < props.length; i++) {
        const prop = props[i]
        if (prop.type === NodeTypes.DIRECTIVE && isMatch(name, prop.name)) {
          fn(node, prop, context)
        }
      }
    }
  }
}

例如,v-if指令会被转换为块:

// 模板: <div v-if="show">content</div>
function render(_ctx) {
  return (_ctx.show)
    ? (_openBlock(), _createBlock("div", { key: 0 }, "content"))
    : _createCommentVNode("v-if", true)
}

自定义指令转换

自定义指令在转换阶段也会被处理,转换为对应的运行时指令对象:

function transformDirectives(node: ElementNode, context: TransformContext) {
  const directives = []
  for (const prop of node.props) {
    if (prop.type === NodeTypes.DIRECTIVE) {
      directives.push({
        name: prop.name,
        rawName: prop.rawName,
        value: prop.exp,
        arg: prop.arg,
        modifiers: prop.modifiers
      })
    }
  }
  if (directives.length) {
    node.directives = directives
  }
}

转换后的指令会合并到VNode的props中:

_createVNode("div", { 
  __directives: {
    myDirective: {
      mounted(el, binding) { /* ... */ }
    }
  }
})

插槽转换

插槽内容在转换阶段会被特殊处理,转换为插槽函数:

function transformSlotOutlet(node, context) {
  if (node.type === NodeTypes.ELEMENT && node.tag === 'slot') {
    const slotName = node.props.find(p => p.name === 'name')?.value || '"default"'
    const slotProps = node.props.filter(p => p.name !== 'name')
    
    node.codegenNode = createSlotOutlet(
      slotName,
      slotProps.length ? createObjectExpression(slotProps) : undefined
    )
  }
}

例如,一个具名插槽会被转换为:

_renderSlot(_ctx.$slots, "header", { item: _ctx.item })

静态类型推导

转换阶段还会进行一些静态分析,推导出表达式的类型信息,这些信息会在代码生成阶段被使用:

function inferExpressionType(
  node: SimpleExpressionNode,
  context: TransformContext
) {
  if (node.isStatic) {
    node.type = 'Literal'
  } else if (context.identifiers.has(node.content)) {
    node.type = 'Identifier'
  } else if (node.content.startsWith('_ctx.')) {
    node.type = 'MemberExpression'
  }
}

这些类型信息可以帮助生成更优化的运行时代码,例如知道某个表达式是简单的属性访问,就可以生成直接的属性访问代码而不是全功能的解析代码。

源码结构分析

转换阶段的主要逻辑位于@vue/compiler-core包的src/transform.ts文件中。核心转换器由多个子转换器组成,每个负责处理特定的转换任务:

transform
├── transformElement       // 处理元素节点
├── transformText         // 处理文本节点
├── transformExpression   // 处理表达式
├── transformIf          // 处理v-if
├── transformFor         // 处理v-for
├── transformOn          // 处理事件
├── transformBind        // 处理v-bind
├── transformModel       // 处理v-model
└── ...                  // 其他转换

这种模块化的设计使得每个转换功能可以独立开发和测试,同时也方便按需组合使用。

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌