Skip to content

1.generate

codegen 模块中的 generate 函数支持将 AST 转化为 render 字符串

ts
export function generate(
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast
    ? ast.tag === 'script'
      ? 'null'
      : genElement(ast, state)
    : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

其中 genElement 方法,利用递归的方式处理各种情况的节点:

js
export function genElement(el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      const maybeComponent = state.maybeComponent(el)
      if (!el.plain || (el.pre && maybeComponent)) {
        data = genData(el, state)
      }

      let tag: string | undefined
      // check if this is a component in <script setup>
      const bindings = state.options.bindings
      if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
        tag = checkBindingType(bindings, el.tag)
      }
      if (!tag) tag = `'${el.tag}'`

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c(${tag}${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

2.helpers

以上节 generate 的执行结果为例:

js
{
  "render": "with(this){return _c('div',[_c('div',[_v(_s(msg))]),_v(\" \"),_m(0),_v(\" \"),_m(1),_v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(show),expression:\"show\"}]},[_c('span',[_v(\"节点3\")])],1)],1)}",
  "staticRenderFns": [
      "with(this){return _c('div',[_c('span',[_v(\"节点1\")])],1)}",
      "with(this){return _c('div',[_c('span',[_v(\"节点2\")])],1)}"
  ]
}

可以看出,存在 _c_v_s 等缩写方法。

这类方法是 Vue 的内置缩写方法,简短命名有利于渲染性能。各个缩写方法的主要含义如下:

js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

export function installRenderHelpers(target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

3.setup

genElement 函数中,存在对于 script setup 的特殊处理:

js
const bindings = state.options.bindings
if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
  tag = checkBindingType(bindings, el.tag)
}

核心目的是,用于确认组件引用

vue
<script setup>
  import MyButton from './MyButton.vue'
  const title = ref('Hello')
</script>

<template>
  <MyButton>{{ title }}</MyButton>
</template>

state.options.bindings 其实由自定义 compileOptions 属性传入,而 compileOptions 是在解析完 script 之后,传入到 template 解析过程中的。

  1. 首先在 SFC 编译入口 compileTemplate 中:
js
export function compileTemplate(
  options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
  const { source, filename, id, scoped, slotted, ssr, ssrCssVars, bindings } = options
  
  // 创建转换上下文
  const transformContext = createTransformContext(
    root,
    {
      // ... 其他选项
      bindings,  // 将 bindings 传入编译选项
      bindingMetadata: bindings // 向后兼容
    }
  )
  
  // 编译模板
  const { code, ast } = baseCompile(template, {
    // ... 其他选项
    bindings,  // bindings 会被传入基础编译器
  })
}
  1. 在实际使用时,通常是通过 compile 函数将整个 SFC 编译:
js
export function compile(source: string, options: CompilerOptions) {
  // 1. 解析 SFC 为不同的块
  const descriptor = parse(source)
  
  // 2. 首先编译 script
  const scriptResult = compileScript(descriptor, options)
  const { bindings } = scriptResult
  
  // 3. 编译 template,并传入 bindings
  const templateResult = compileTemplate({
    // ... 其他选项
    bindings, // 在这里传入 bindings
    filename: options.filename
  })
}
  1. 最终这些 bindings 会传递到模板编译器的代码生成阶段:
js
export function generate(
  ast: ASTElement | void,
  options: CompilerOptions // 包含了 bindings
): CodegenResult {
  const state = new CodegenState(options) // options 中包含 bindings
  
  // ... 生成代码
}

// 在生成元素代码时使用
export function genElement(el: ASTElement, state: CodegenState): string {
  // ...
  const bindings = state.options.bindings // 这里就能访问到 bindings
  if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
    tag = checkBindingType(bindings, el.tag)
  }
  // ...
}

一个完整的例子:

vue
<script setup>
import MyButton from './MyButton.vue'
const title = ref('Hello')
</script>

<template>
  <MyButton>{{ title }}</MyButton>
</template>

编译过程:

js
// 1. 解析 script setup,生成 bindings
const bindings = {
  'MyButton': BindingTypes.SETUP_CONST,
  'title': BindingTypes.SETUP_REF
}

// 2. 编译模板时传入 bindings
compileTemplate({
  source: '<MyButton>{{ title }}</MyButton>',
  bindings,
  // ...其他选项
})

// 3. 在代码生成时使用 bindings
// - 确认 MyButton 是一个组件
// - 知道 title 是一个 ref,需要自动解包

生成的代码大致会是:

js
import { toDisplayString as _toDisplayString } from "vue"

export function render(_ctx, _cache) {
  return _openBlock(), _createBlock(_ctx.MyButton, null, {
    default: _withCtx(() => [
      _toDisplayString(_ctx.title)
    ]),
    _: 1
  })
}