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
解析过程中的。
- 首先在
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 会被传入基础编译器
})
}
- 在实际使用时,通常是通过
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
})
}
- 最终这些
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
})
}