vapor 的 IR 是如何被 generate 到 render 函数的

vapor template 编译的三个流程分别为 parse,transform,generate

parse 是将 source 解析成 AST,transform 是将 AST 转换为 IR,generate 是将 IR 生成 render 函数

前两个步骤已经在上两篇文章中讲过了,感兴趣可以往上找找,那么本期文章就介绍 generate 这一步骤

我们还是以下面这个简单的 template 举例

vue 复制代码
<template>
    <h2 v-if="true">{{ title }}</h2>
</template>

得到的 ir 如下所示

从 ir generate 到最终生成的 js 代码 (主要是 render 函数)如下所示

js 复制代码
import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<h2> </h2>")
const t1 = _template("<template></template>", true)

export function render(_ctx, $props, $emit, $attrs, $slots) {
  const n3 = t1()
  _setInsertionState(n3)
  const n0 = _createIf(() => (true), () => {
    const n2 = t0()
    const x2 = _child(n2)
    _renderEffect(() => _setText(x2, _toDisplayString(_ctx.title)))
    return n2
  })
  return n3
}

ir 是如何变成 render 函数就是我们接下来要探讨的内容

generate

可以看到 generate 的两个入参分别为 transform 后得到的 ir,以及 从 compiler-dom 层传入的 resolvedOptions vapor 信息

深入到 generate 中,内容还是比较多,咱们一个步骤一个步骤来

ts 复制代码
// IR -> JS codegen
export function generate(
  ir: RootIRNode,
  options: CodegenOptions = {},
): VaporCodegenResult {
  // 1. 初始化
  const [frag, push] = buildCodeFragment()
  const context = new CodegenContext(ir, options)
  const { helpers } = context
  const { inline, bindingMetadata } = options
  const functionName = 'render'
  // 2. 函数签名
  const args = ['_ctx']
  if (bindingMetadata && !inline) {
    // binding optimization args
    args.push('$props', '$emit', '$attrs', '$slots')
  }
  const signature = (options.isTS ? args.map(arg => `${arg}: any`) : args).join(
    ', ',
  )
  // 3. 函数开始标记
  if (!inline) {
    push(NEWLINE, `export function ${functionName}(${signature}) {`)
  }

  push(INDENT_START)
  // 4. 模板引用处理
  if (ir.hasTemplateRef) {
    push(
      NEWLINE,
      `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
    )
  }
  // 5. 🔥核心:生成主要内容
  push(...genBlockContent(ir.block, context, true))
  // 6. 函数标记结束
  push(INDENT_END, NEWLINE)

  if (!inline) {
    push('}')
  }
  // 7. 前置代码
  const delegates = genDelegates(context)
  const templates = genTemplates(ir.template, ir.rootTemplateIndex, context)
  const imports = genHelperImports(context)
  const preamble = imports + templates + delegates
  // 8. 源码映射处理
  const newlineCount = [...preamble].filter(c => c === '\n').length
  if (newlineCount && !inline) {
    frag.unshift(...new Array<CodeFragment>(newlineCount).fill(LF))
  }
  // 9. 最终代码生成
  let [code, map] = codeFragmentToString(frag, context)
  if (!inline) {
    code = preamble + code
  }

  return {
    code,
    ast: ir,
    preamble,
    map: map && map.toJSON(),
    helpers,
  }
}

从最后 return 的返回值中可以看出,里面有多个值,其中 code 才是我们想要弄清楚的 render 函数

第一步:初始化

ts 复制代码
  const [frag, push] = buildCodeFragment()
  const context = new CodegenContext(ir, options)
  const { helpers } = context
  const { inline, bindingMetadata } = options
  const functionName = 'render'

buildCodeFragment 函数会提供一个 frag 数组和对应的 push方法,frag 数组是用来存储生成的代码片段

context 是 generate 的上下文实例对象,只需要传入 ir,以及 resolvedOptions 就能得到一个 context,context 身上会记录代码生成的状态以及一些 函数,比如 helpers 就记录了导入的运行时辅助函数

第二步:生成函数签名

ts 复制代码
  const args = ['_ctx']
  if (bindingMetadata && !inline) {
    // binding optimization args
    args.push('$props', '$emit', '$attrs', '$slots')
  }
  const signature = (options.isTS ? args.map(arg => `${arg}: any`) : args).join(
    ', ',
  )

_ctx 是组件上下文-基础参数,当绑定元数据并且非内联模式时,会添加 一些 属性值,生成的 签名会 拼接 any 类型,支持 ts

第三步:函数开始标记

ts 复制代码
  if (!inline) {
    push(NEWLINE, `export function ${functionName}(${signature}) {`)
  }

  push(INDENT_START)

非内联模式会给代码片段 frag push render 函数的第一行,也就是

js 复制代码
export function render(_ctx, $props, $emit, $attrs, $slots) {

像是 NEWLINE ,INDENT_START 都是一个标记,分别为 新启一行,开始切割

第四步:模板引用处理

ts 复制代码
  if (ir.hasTemplateRef) {
    push(
      NEWLINE,
      `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
    )
  }

若模板有 ref 引用属性,就会生成模板引用设置起,运行时就会调用createTemplateRefSetter 辅助函数

第五步:生成主要内容

ts 复制代码
  push(...genBlockContent(ir.block, context, true))

在 debug 时,我们实时观察 frag 的变化,目前的 frag 已经如下所示

js 复制代码
[
    Symbol(newline),
    "export function render(_ctx, $props, $emit, $attrs, $slots) {",
    Symbol(indent start)
]

在运行完 genBlockContent 时,我们可以看看 frag 的变化

js 复制代码
[
    Symbol(newline),
    "export function render(_ctx, $props, $emit, $attrs, $slots) {",
    Symbol(indent start),
    Symbol(newline),
    "const n3 = t1()",
    Symbol(newline),
    "_setInsertionState",
    "(",
    "n3",
    ")",
    Symbol(newline),
    "const n0 = ",
    "_createIf",
    "(",
    "() => (",
    [
        "true",
        -2,
        {
            "start": {
                "column": 15,
                "line": 2,
                "offset": 26
            },
            "end": {
                "column": 19,
                "line": 2,
                "offset": 30
            },
            "source": "true"
        }
    ],
    undefined,
    ")",
    ", ",
    "(",
    ") => {",
    Symbol(indent start),
    Symbol(newline),
    "const n2 = t0()",
    Symbol(newline),
    "const x2 = _child(n2)",
    Symbol(newline),
    "_renderEffect(() => ",
    "_setText",
    "(",
    "x2",
    ", ",
    "_toDisplayString",
    "(",
    undefined,
    [
        "_ctx.title",
        -2,
        {
            "start": {
                "column": 24,
                "line": 2,
                "offset": 35
            },
            "end": {
                "column": 29,
                "line": 2,
                "offset": 40
            },
            "source": "title"
        },
        "title"
    ],
    ")",
    ")",
    ")",
    Symbol(newline),
    "return ",
    "n2",
    Symbol(indent end),
    Symbol(newline),
    "}",
    ")",
    Symbol(newline),
    "return ",
    "n3"
]

可以看到,基本上把 render 函数所有的代码片段生成完了,就差一个 }, generate 后面会进行拼接,因此这个函数才是我们需要重点观察的对象,因为 ir 的转换就在这里实现

genBlockContent

明白了 genBlockContent 的作用后,我们深入这个函数,看它如何实现

ts 复制代码
export function genBlockContent(
  block: BlockIRNode,
  context: CodegenContext,
  root?: boolean,
  genEffectsExtraFrag?: () => CodeFragment[],
): CodeFragment[] {
  const [frag, push] = buildCodeFragment()
  const { dynamic, effect, operation, returns } = block
  const resetBlock = context.enterBlock(block)

  if (root) {
    // 处理组件解析
    for (let name of context.ir.component) {
      const id = toValidAssetId(name, 'component')
      const maybeSelfReference = name.endsWith('__self')
      if (maybeSelfReference) name = name.slice(0, -6)
      push(
        NEWLINE,
        `const ${id} = `,
        ...genCall(
          context.helper('resolveComponent'),
          JSON.stringify(name),
          // pass additional `maybeSelfReference` flag
          maybeSelfReference ? 'true' : undefined,
        ),
      )
    }
    genResolveAssets('directive', 'resolveDirective')
  }
  // 动态节点自身生成
  for (const child of dynamic.children) {
    push(...genSelf(child, context))
  }
  // 动态子节点元素生成
  for (const child of dynamic.children) {
    push(...genChildren(child, context, push, `n${child.id!}`))
  }
  // 操作代码生成
  push(...genOperations(operation, context))
  // effect 代码生成
  push(...genEffects(effect, context, genEffectsExtraFrag))

  push(NEWLINE, `return `)

  const returnNodes = returns.map(n => `n${n}`)
  const returnsCode: CodeFragment[] =
    returnNodes.length > 1
      ? genMulti(DELIMITERS_ARRAY, ...returnNodes)
      : [returnNodes[0] || 'null']
  push(...returnsCode)

  resetBlock()
  return frag

  function genResolveAssets(
    kind: 'component' | 'directive',
    helper: CoreHelper,
  ) {
    for (const name of context.ir[kind]) {
      push(
        NEWLINE,
        `const ${toValidAssetId(name, kind)} = `,
        ...genCall(context.helper(helper), JSON.stringify(name)),
      )
    }
  }
}

genBlockContent 接收了三个入参,分别为 ir.block,generateContext 以及 一个 代表 根节点的 布尔值

我们先看下此时的 ir.block 长啥样

block 是个 BlockIRNode 类型的对象,会包含如下 属性

  • type: IRNodeTypes.BLOCK (值为 1)
  • node: 对应的 ast 节点
  • temId: 临时 ID,用于生成代码时的变量命名
  • children: 包含一个子元素的动态信息
  • flags: 动态标志位,表示节点的动态特性
  • hasDynamicChild: 表示是否有动态子节点

genBlockContent 同样会利用 frags 存放代码片段

resetBlock = context.enterBlock 会重置当前的 block

随后进入 root if 分支中,因为我们在执行 genBlockContent 时,已经将根节点 为 true 的信息作为入参传入了进去

ts 复制代码
  if (root) {
    // 处理组件解析
    for (let name of context.ir.component) {
      const id = toValidAssetId(name, 'component')
      const maybeSelfReference = name.endsWith('__self')
      if (maybeSelfReference) name = name.slice(0, -6)
      push(
        NEWLINE,
        `const ${id} = `,
        ...genCall(
          context.helper('resolveComponent'),
          JSON.stringify(name),
          // pass additional `maybeSelfReference` flag
          maybeSelfReference ? 'true' : undefined,
        ),
      )
    }
    genResolveAssets('directive', 'resolveDirective')
  }

只在根块中生成组件和指令的解析代码

里面的遍历是为了生成 resolveComponent 调用,处理自引用组件(_self 后缀)

genResolveAssets 会处理指令解析

js 复制代码
  for (const child of dynamic.children) {
    push(...genSelf(child, context))
  }

这里会为 每个动态子节点 生成其自身的创建代码,genSelf 就是生成节点创建语句

比如 const n0 = template("<h2></h2>")

此时的 dynamic 是根块的 属性,里面的 child 也只有一个

我们看下 genSelf 是如何创建 block 代码的

genSelf & genChildren

genSelf 作用是生成动态节点自身的创建代码,如果节点有 id 和 template 属性,生成 const n${id} = t${template}()

调用 genDirectivesForElement 处理该元素上的指令

如果有 operation ,调用 genOperationWithInsertionState 处理

在执行 push(NEWLINE, const n${id} = t${template}()) 时就对应此时的 "const n3 = t1()"

此时我们就明白了,原来 vapor 下的块语句是由 genSelf 创建的,这个拿到了 block.dynamic 的 id 和 template 用字符串拼接而成

由于此时 根块的子节点 block 节点不包含 operation 信息,因此没有走入 genOperationWithInsertionState 函数中,这个函数稍后解释

接下来看 genChildren

ts 复制代码
for (const child of dynamic.children) {
  push(...genChildren(child, context, push, `n${child.id!}`))
}

这里其实就是对子节点进行递归,我可以先看下在上一步 genSelf 时的 frag 信息是怎样的

再看下 执行完 genChildren 后的 frag 又是怎样的

因此,我们肯定可以判断出,里面肯定像是 transformChildren 那样去层序遍历子节点,去调用 genBlockContent,当然最后肯定是靠 genSelf 生成 block 块的

我们同样看下 genChildren 长啥样

ts 复制代码
export function genChildren(
  dynamic: IRDynamicInfo,
  context: CodegenContext,
  pushBlock: (...items: CodeFragment[]) => number,
  from: string = `n${dynamic.id}`,
): CodeFragment[] {
  const { helper } = context
  const [frag, push] = buildCodeFragment()
  const { children } = dynamic

  let offset = 0
  let prev: [variable: string, elementIndex: number] | undefined
  const childrenToGen: [IRDynamicInfo, string][] = []

  for (const [index, child] of children.entries()) {
    if (child.flags & DynamicFlag.NON_TEMPLATE) {
      offset--
    }

    const id =
      child.flags & DynamicFlag.REFERENCED
        ? child.flags & DynamicFlag.INSERT
          ? child.anchor
          : child.id
        : undefined

    if (id === undefined && !child.hasDynamicChild) {
      push(...genSelf(child, context))
      continue
    }

    const elementIndex = Number(index) + offset
    // p for "placeholder" variables that are meant for possible reuse by
    // other access paths
    const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
    pushBlock(NEWLINE, `const ${variable} = `)

    if (prev) {
      if (elementIndex - prev[1] === 1) {
        pushBlock(...genCall(helper('next'), prev[0]))
      } else {
        pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
      }
    } else {
      if (elementIndex === 0) {
        pushBlock(...genCall(helper('child'), from))
      } else {
        // check if there's a node that we can reuse from
        let init = genCall(helper('child'), from)
        if (elementIndex === 1) {
          init = genCall(helper('next'), init)
        } else if (elementIndex > 1) {
          init = genCall(helper('nthChild'), from, String(elementIndex))
        }
        pushBlock(...init)
      }
    }

    if (id === child.anchor) {
      push(...genSelf(child, context))
    }

    if (id !== undefined) {
      push(...genDirectivesForElement(id, context))
    }

    prev = [variable, elementIndex]
    childrenToGen.push([child, variable])
  }

  if (childrenToGen.length) {
    for (const [child, from] of childrenToGen) {
      push(...genChildren(child, context, pushBlock, from))
    }
  }

  return frag
}

为了方便理解,其实 genChildren 简化下逻辑就是如下

ts 复制代码
// genChildren 递归处理子节点
export function genChildren(dynamic: IRDynamicInfo, context: CodegenContext) {
  for (const [child, variable] of childrenToGen) {
    push(...genChildren(child, context, pushBlock, variable)) // 递归调用
  }
}

genChildren 前面依旧是初始化 frag 数组等等逻辑

有个 offset 的遍历,offset 是用于调整索引,因为非模板节点不会占用 dom 位置,这就是计算每个子节点在 dom 中的实际位置

if (prev) { ... } 是节点访问优化

对于相邻节点使用 next(prevNode)而不是重新查找,对于首个子节点使用 child(parent),对于任意位置使用 nthChild(parent, index)

现在关注到下面这个代码块

ts 复制代码
    if (id === undefined && !child.hasDynamicChild) {
      push(...genSelf(child, context))
      continue
    }

此时的 child 已经来到了 v-if 的 节点,这里会进入 genSelf,看下此时的 dynamic 和 dynamic.opration

在根块下,operation 不存在,这时候 v-if 就存在了,我们看下将会如何执行 genOperationWithInsertionState 函数

ts 复制代码
export function genOperationWithInsertionState(
  oper: OperationNode,
  context: CodegenContext,
): CodeFragment[] {
  const [frag, push] = buildCodeFragment()
  if (isBlockOperation(oper) && oper.parent) {
    push(...genInsertionState(oper, context))
  }
  push(...genOperation(oper, context))
  return frag
}

这个函数目的是处理操作节点并管理插入状态

isBlockOperation 函数会检测 operation 是否为块操作,包括 oper 是否含有父节点

isBlockOperation 块操作类型包含如下

ts 复制代码
  return (
    type === IRNodeTypes.CREATE_COMPONENT_NODE ||
    type === IRNodeTypes.SLOT_OUTLET_NODE ||
    type === IRNodeTypes.IF ||
    type === IRNodeTypes.FOR
  )
  • v-if / v-else-if / v-else 条件渲染
  • v-for 渲染渲染
  • 组件渲染
  • 插槽

我们这里肯定满足条件,if 的 type 为 15,for 为 16,这是 IRNodeTypes

ts 复制代码
export enum IRNodeTypes {
  ROOT,
  BLOCK,

  SET_PROP,
  SET_DYNAMIC_PROPS,
  SET_TEXT,
  SET_EVENT,
  SET_DYNAMIC_EVENTS,
  SET_HTML,
  SET_TEMPLATE_REF,

  INSERT_NODE,
  PREPEND_NODE,
  CREATE_COMPONENT_NODE,
  SLOT_OUTLET_NODE,

  DIRECTIVE,
  DECLARE_OLD_REF, // consider make it more general

  IF,
  FOR,

  GET_TEXT_CHILD,
}

因此满足条件会执行 genInsertionState 函数

ts 复制代码
function genInsertionState(
  operation: InsertionStateTypes,
  context: CodegenContext,
): CodeFragment[] {
  return [
    NEWLINE,
    ...genCall(
      context.helper('setInsertionState'),
      `n${operation.parent}`,
      operation.anchor == null
        ? undefined
        : operation.anchor === -1 // -1 indicates prepend
          ? `0` // runtime anchor value for prepend
          : `n${operation.anchor}`,
    ),
  ]
}

genInsertionState 生成的代码将会如下

md 复制代码
setInsertionState(n0, n1)  // 设置父节点和锚点
setInsertionState(n0)      // 只设置父节点,追加到末尾
setInsertionState(n0, 0)   // 设置父节点,前置插入

接下来会执行 push(...genOperation(oper, context))

genOperation 如下

ts 复制代码
export function genOperation(oper: OperationNode, context: CodegenContext) {
  switch (oper.type) {
    case IRNodeTypes.IF:
      return genIf(oper, context)
    case IRNodeTypes.FOR:
      return genFor(oper, context)
    case IRNodeTypes.CREATE_COMPONENT_NODE:
      return genCreateComponent(oper, context)
    case IRNodeTypes.SET_TEXT:
      return genSetText(oper, context)
    // ... 其他操作类型
  }
}

我们会执行到 genIf

genIf
ts 复制代码
export function genIf(
  oper: IfIRNode,
  context: CodegenContext,
  isNested = false,
): CodeFragment[] {
  const { helper } = context
  const { condition, positive, negative, once } = oper
  const [frag, push] = buildCodeFragment()

  const conditionExpr: CodeFragment[] = [
    '() => (',
    ...genExpression(condition, context),
    ')',
  ]

  let positiveArg = genBlock(positive, context)
  let negativeArg: false | CodeFragment[] = false

  if (negative) {
    if (negative.type === IRNodeTypes.BLOCK) {
      negativeArg = genBlock(negative, context)
    } else {
      negativeArg = ['() => ', ...genIf(negative!, context, true)]
    }
  }

  if (!isNested) push(NEWLINE, `const n${oper.id} = `)
  push(
    ...genCall(
      helper('createIf'),
      conditionExpr,
      positiveArg,
      negativeArg,
      once && 'true',
    ),
  )

  return frag
}

const conditionExpr: 将条件包装成 函数,实现响应式

let positiveArg = genBlock(positive, context)

这里是正向分支处理,为 v-if 的内容生成块函数

ts 复制代码
export function genBlock(
  oper: BlockIRNode,
  context: CodegenContext,
  args: CodeFragment[] = [],
  root?: boolean,
): CodeFragment[] {
  return [
    '(',
    ...args,
    ') => {',
    INDENT_START,
    ...genBlockContent(oper, context, root),
    INDENT_END,
    NEWLINE,
    '}',
  ]
}

可以看到这里调用了 genBlockContent

其实就是 genIf 会调用 genBlock 拿到 positiveArg,genBlock 又会调用 genBlockContent,genBlockContent 会处理 v-if 块的内容

我们直接看到 positiveArg 内容如何

可以看到这是 v-if 条件为 true 时要执行的代码片段

positiveArg 最终会作为 参数传递给运行时 createIf 函数

md 复制代码
[
  "(",           // 0: 函数开始括号
  ") => {",      // 1: 箭头函数语法
  Symbol(indent_start), // 2: 缩进开始标记
  Symbol(newline),      // 3: 换行符
  "const n2 = t1()",    // 4: 创建 DOM 节点
  // ...
]
"const n2 = t1()"  // 调用模板函数创建 <h2> 元素
[
  Symbol(newline),     // 7: 换行
  "setText",           // 8: setText 函数调用
  "(",                 // 9: 参数开始
  "n2",               // 10: 目标节点
  ", ",               // 11: 参数分隔符
  "_ctx.title",       // 12: 绑定的数据
  ", ",               // 13: 参数分隔符  
  "[",                // 14: 数组开始
  "]",                // 15: 数组结束
  ")",                // 16: 函数调用结束
]
[
  Symbol(newline),     // 17: 换行
  "return ",          // 18: return 关键字
  "n2"                // 19: 返回创建的节点
]
[
  Symbol(indent_end),  // 20: 缩进结束标记
  Symbol(newline),     // 21: 换行
  "}"                  // 22: 函数结束括号
]

接下来是 负向分支处理

ts 复制代码
let negativeArg: false | CodeFragment[] = false

if (negative) {
  if (negative.type === IRNodeTypes.BLOCK) {
    negativeArg = genBlock(negative, context)
  } else {
    negativeArg = ['() => ', ...genIf(negative!, context, true)]
  }
}

负向分支就会有两种情况,一个 v-else 块,一个 v-else-if 链

v-else-if 链又会进行递归处理

我们可以看个示例

vue 复制代码
<template>
  <div v-if="showA">A</div>
  <div v-else-if="showB">B</div>
  <div v-else>C</div>
</template>

这个 template 那么会生成大概下面这样的 函数

js 复制代码
const n0 = createIf(
  () => (_ctx.showA),           // 条件函数
  () => {                       // positive 分支
    const n1 = t0()             // <div>A</div>
    setText(n1, "A")
    return n1
  },
  () => createIf(               // negative 分支(递归)
    () => (_ctx.showB),         // v-else-if 条件
    () => {                     // v-else-if 内容
      const n2 = t1()           // <div>B</div>
      setText(n2, "B")
      return n2
    },
    () => {                     // v-else 内容
      const n3 = t2()           // <div>C</div>
      setText(n3, "C")
      return n3
    }
  )
)

我们现在看 genIf 最后的代码生成

ts 复制代码
  if (!isNested) push(NEWLINE, `const n${oper.id} = `)
  push(
    ...genCall(
      helper('createIf'),
      conditionExpr,
      positiveArg,
      negativeArg,
      once && 'true',
    ),
  )

isNested 判断是否有嵌套

顶层 v-if 会生成 const n0 = createIf(...)

嵌套的 v-else-if 则只生成 createIf(...)

并且往 frag 中塞了个 helper 运行时函数,我们大概看下 createIf

ts 复制代码
export function createIf(
  condition: () => any,
  b1: BlockFn,
  b2?: BlockFn,
  once?: boolean,
): Block {
  // 运行时条件渲染逻辑
  let frag: Block
  if (once) {
    frag = condition() ? b1() : b2 ? b2() : []
  } else {
    frag = new DynamicFragment()
    renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
  }
  return frag
}

注意到里面有 renderEffect 函数

genCall 作用是生成 函数调用的代码片段

ts 复制代码
export function genCall(
  name: string | [name: string, placeholder?: CodeFragments],
  ...frags: CodeFragments[]
): CodeFragment[] {
  const hasPlaceholder = isArray(name)
  const fnName = hasPlaceholder ? name[0] : name
  const placeholder = hasPlaceholder ? name[1] : 'null'
  return [fnName, ...genMulti(['(', ')', ', ', placeholder], ...frags)]
}

可以看到 v-if 的 ir 会先被设置 插入状态 setInsertionState(n0, n1)

然后 genif,也就是 生成具体操作

ts 复制代码
// 1. 设置插入状态(如果是块操作且有父节点)
setInsertionState(n0, n1)

// 2. 生成具体操作
const n2 = createIf(
  () => (true),
  () => {
    const n3 = t1()  // <h2></h2>
    setText(n3, _ctx.title)
    return n3
  }
)
递归关系

可能已经被递归关系搞混乱了,我们现在来梳理下这个递归关系

md 复制代码
genBlockContent (根块)
├── genSelf(dynamic.children[0])
│   └── genOperationWithInsertionState(operation)
│       └── genOperation(operation)
│           ├── genIf(ifOperation)
│           │   └── genBlock(positive/negative)
│           │       └── genBlockContent(nested) ← 递归!
│           ├── genFor(forOperation)  
│           │   └── genBlockContent(render) ← 直接递归!
│           └── genSlotOutlet(slotOperation)
│               └── genBlock(fallback)
│                   └── genBlockContent(nested) ← 递归!
├── genChildren(dynamic.children[0])
│   └── genChildren(child) ← 递归处理子节点
└── genOperations(operation)
    └── 同上操作处理

可以看到,递归终止条件为 叶子节点 没有子节点,dynamic.children.length === 0

对于我们写的简单模板 <h2 v-if="true">{{ title }}</h2>

那么这个递归调用流程即

md 复制代码
// 生成的递归调用流程:
genBlockContent(rootBlock)
├── genSelf() // 处理根节点
├── genChildren() // 处理子节点
└── genOperations() // 处理 v-if 操作
    └── genIf()
        └── genBlock(positiveBlock) // v-if 为 true 时的块
            └── genBlockContent(positiveBlock) // 递归调用!
                ├── genSelf() // 生成 h2 元素
                └── genOperations() // 处理 {{ title }} 文本设置

所以 genSelf 和 genChildren 通过一系列操作最终会调用 genBlockContent

genEffects

genEffects 负责生成响应式更新的代码

比如

js 复制代码
renderEffect(() => {
  setText(n0, _ctx.str)
})

这部分我后面文章再讲

现在回到 genBlockContent 中来,已经到了第六步

第六步:函数结束标记

ts 复制代码
push(INDENT_END, NEWLINE)
if (!inline) {
  push('}')
}

给 render 函数补上最后一个 }

第七步:生成前置代码

preamble 前置代码生成

前置代码指的是 render 函数前面的一些 import 和 block 定义

js 复制代码
import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<h2> </h2>")
const t1 = _template("<template></template>", true)

我们看下如何生成的前置代码

ts 复制代码
const delegates = genDelegates(context)
const templates = genTemplates(ir.template, ir.rootTemplateIndex, context)
const imports = genHelperImports(context)
const preamble = imports + templates + delegates

delegates 是事件委托

ts 复制代码
function genDelegates({ delegates, helper }: CodegenContext) {
  return delegates.size
    ? genCall(
        helper('delegateEvents'),
        ...Array.from(delegates).map(v => `"${v}"`),
      ).join('') + '\n'
    : ''
}

比如 click 事件,则会有 _delegateEvents("click", "input")

render 前面的 template 函数则是利用 genTemplates 实现

ts 复制代码
export function genTemplates(
  templates: string[],
  rootIndex: number | undefined,
  { helper }: CodegenContext,
): string {
  return templates
    .map(
      (template, i) =>
        `const t${i} = ${helper('template')}(${JSON.stringify(
          template,
        )}${i === rootIndex ? ', true' : ''})\n`,
    )
    .join('')
}

比如最后的 t0 就是 h2 :const t0 = _template("<h2></h2>")

最后的 js 代码的 imports 则是主要靠 const imports = genHelperImports(context)

ts 复制代码
function genHelperImports({ helpers, helper, options }: CodegenContext) {
  let imports = ''
  if (helpers.size) {
    imports += `import { ${[...helpers]
      .map(h => `${h} as _${h}`)
      .join(', ')} } from '${options.runtimeModuleName}';\n`
  }
  return imports
}

可以看到直接把前面的 helpers 拿到后进行了拼接

js 复制代码
import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';

第八步:将前置代码前插入frag中

ts 复制代码
const newlineCount = [...preamble].filter(c => c === '\n').length
if (newlineCount && !inline) {
  frag.unshift(...new Array<CodeFragment>(newlineCount).fill(LF))
}

第九步:最终代码生成

ts 复制代码
let [code, map] = codeFragmentToString(frag, context)
if (!inline) {
  code = preamble + code
}

codeFragmentToString 就是将 frag 生成最终的 code,也就是 render 函数

codeFragmentToString

这个函数则是拿到最终的 frag 代码片段数组转换为 最终的 字符串代码

这里我们简化下 codeFragmentToString 函数

ts 复制代码
function codeFragmentToString(code: CodeFragment[]): string {
  let result = ''
  let indentLevel = 0

  for (let frag of code) {
    if (!frag) continue  // 跳过空值

    // 处理特殊符号
    if (frag === NEWLINE) {
      result += `\n${'  '.repeat(indentLevel)}`  // 换行 + 缩进
    } else if (frag === INDENT_START) {
      indentLevel++  // 增加缩进级别
    } else if (frag === INDENT_END) {
      indentLevel--  // 减少缩进级别
    } else {
      // 普通字符串直接拼接
      result += frag
    }
  }

  return result
}

这里拿到 frags 数组,每个进行遍历,会对特殊的符号进行特殊处理,比如 换行缩进这种,普通的字符就是直接进行拼接,这里还用了一个 indentLevel 跟踪当前缩进级别

第十步:return

总结

generate 阶段在拿到 ir 后,会去不断 push 代码片段,其中最核心的步骤就是 genBlockContent 函数帮我们拿到主要的 render 函数的代码片段,这个函数的主要实现是 genSelf 以及 genChildren 不断递归,里面会记录每个节点的 动态操作,插入状态,指令等,并且每个动态部分都会有独立的响应式更新,生成的代码能够直接调用 dom api

这也就是为啥 vapor 下相较 v3 无需 vdom 也能实现精确的 dom 操作,vapor 在编译阶段就已经生成了必要的dom操作代码

拿到了 render 函数的所有代码片段后去拿到 前置代码 preamble,也就是 import 语句那些,然后 unshift 到 frag 中,最后通过 codeFragmentToString 拼接成最后的 code ,也就是我们看到的最后解析的 render 函数

相关推荐
pe7er几秒前
Tauri 应用打包与签名简易指南
前端
前端搬砖仔噜啦噜啦嘞2 分钟前
Cursor AI 编辑器入门教程和实战
前端·架构
Jimmy16 分钟前
TypeScript 泛型:2025 年终极指南
前端·javascript·typescript
来来走走21 分钟前
Flutter dart运算符
android·前端·flutter
moddy26 分钟前
新人怎么去做低代码,并且去使用?
前端
风清云淡_A27 分钟前
【Flutter3.8x】flutter从入门到实战基础教程(五):Material Icons图标的使用
前端·flutter
安心不心安32 分钟前
React ahooks——副作用类hooks之useThrottleEffect
前端·react.js·前端框架
jstart千语34 分钟前
【vue】创建响应式数据ref和reactive的区别
前端·javascript·vue.js
肖师叔爱抽风40 分钟前
使用nvm use切换版本时出现虚假成功信息,使用nvm ls,实际显示没有切换成功,解决方法
前端
猩猩程序员43 分钟前
Rust为什么需要Borrow Trait
前端