什么是 codegen
codegen 用于将 codegenNode 的结构转换为真实的 js 代码,例如:
- 这是我们写的
vue代码:
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{{ msg }}</h1>
</template>
- 这是
codegen最后生成的代码:
/* Analyzed bindings: {
"ref": "setup-const",
"msg": "setup-ref"
} */
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import { ref } from 'vue'
const __sfc__ = {
__name: 'App',
setup(__props) {
const msg = ref('Hello World!')
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(msg.value), 1 /* TEXT */))
}
}
}
__sfc__.__file = "src/App.vue"
export default __sfc__
vue 为了防止函数名冲突,在方法前加了 _,为了看的更加清晰,我们可以进一步简化:
/* Analyzed bindings: {
"ref": "setup-const",
"msg": "setup-ref"
} */
import { toDisplayString, openBlock, createElementBlock } from "vue"
import { ref } from 'vue'
const sfc = {
name: 'App',
setup(props) {
const msg = ref('Hello World!')
return (ctx, cache) => {
return (openBlock(), createElementBlock("h1", null, toDisplayString(msg.value), 1))
}
}
}
sfc.file = "src/App.vue"
export default sfc
我们可以看到最后默认导出了 sfc,这个可以被 @vitejs/plugin-vue 插件识别,进一步生成可执行文件
原理
在 compile 文件中,使用 generate 作为入口函数,并将其返回结果返回
export function baseCompile(template, options) {
// ...
return generate(ast)
}
查看 generate 函数的内容:
export function generate(ast, options = {}) {
const context = createCodegenContext(ast, options);
const { push, mode } = context;
if (mode === "module") {
genModulePreamble(ast, context);
} else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
// 这里需要生成具体的代码内容
// 开始生成 vnode tree 的表达式
push("return ")
genNode(ast.codegenNode, context)
push("}")
return {
code: context.code,
};
}
createCodegenContext
createCodegenContext 函数用于生产上下文:
其返回值是一个对象,内容包括:
code:最终生成可执行的代码helper:用于生成代码的辅助函数push:向code添加内容newline:为code换行
function createCodegenContext(ast, {
runtimeModuleName = 'vue',
runtimeGlobalName = 'vue',
mode = 'function'
}) {
const context = {
code: '',
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`
},
push(code) {
context.code += code
},
newline() {
// TODO: 缩进处理
context.code += '\n'
}
}
return context
}
genModulePreamble
genModulePreamble 就是添加 import 语句的,就是最开始我们看到的:
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
函数代码:
代码很简单,就是生成了一堆
import字符串
function genModulePreamble(ast, context) {
const { push, newline, runtimeModuleName } = context
if(ast.helpers.length) {
const code = `
import {${ast.helpers.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')}} from ${JSON.stringify(runtimeModuleName)}
`
push(code)
}
newline()
push(`export `)
}
先看一下 helperNameMap 是什么吧:
export const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode"
};
genFunctionPreamble
和 genModulePreamble 类似,genFunctionPreamble 是用于添加函数内容的
function genFunctionPreamble(ast, context) {
const { runtimeGlobalName, push, newline } = context
const VueBinging = runtimeGlobalName
const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`
if(ast.helpers.length > 0) {
push(
`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`
);
}
newline()
push(`return `)
}
与 genModulePreamble 不同的是,genFunctionPreamble 是直接从 vue 示例中获取代码,效果其实差不多
添加 render 逻辑
最开始的代码并没有直接出现 render 函数,但他是 createElementBlack 等函数内部的基本方法
return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(msg.value), 1 /* TEXT */))
export function generate(ast, options = {}) {
// ...
const functionName = 'render'
const args = ['_ctx']
const signature = args.join(', ')
push(`function ${functionName}(${signature}) {`)
// 开始生成具体的代码内容
push('return')
genNode(ast.codegenNode, context)
push('}')
return {
code: context.code
}
}
genNode
genNode 代码是通过读取之前 transofrm 转换成的 node,基于不同类型的 node 来生成对应的代码块,最终将代码块拼凑在一起就能形成完整代码了:
function genNode(node, context) {
switch(node.type) {
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.ELEMENT:
genElement(node, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
default:
break
}
}
genInterpolation
用来解析插值语法:
function genInterpolation(node, context) {
const { push, helper } = context
push(`${helper(TO_DISPLAYING_STRING)}(`)
genNode(node.content, context)
push(')')
}
genNode 结构:
codegenNode: {
type: 4,
tag: "'div'",
props: null,
children: {
type: 2, // NodeTypes.INTERPOLATION
content: {
type: 3, // SIMP
content: '_ctx.msg',
},
},
},