发布时间:2022-11-09 21:30
上一节课我们分析了 AST 节点转换的过程,也知道了 AST 节点转换的作用是通过语法分析,创建了语义和信息更加丰富的代码生成节点 codegenNode,便于后续生成代码。
那么这一节课,我们就来分析整个编译的过程的最后一步——代码生成的实现原理。
同样的,代码生成阶段由于要处理的场景很多,所以代码也非常多而复杂。为了方便你理解它的核心流程,我们还是通过这个示例来演示整个代码生成的过程:
class="app">
<hello v-if="flag">hello>
<div v-else>
<p>hello {{ msg + test }}p>
<p>staticp>
<p>staticp>
div>
代码生成的结果是和编译配置相关的,你可以打开官方提供的模板导出工具平台,点击右上角的 Options 修改编译配置。为了让你理解核心的流程,这里我只分析一种配置方案,当然当你理解整个编译核心流程后,也可以修改这些配置分析其他的分支逻辑。
我们分析的编译配置是:mode 为 module,prefixIdentifiers 开启,hoistStatic 开启,其他配置均不开启。
为了让你有个大致印象,我们先来看一下上述例子生成代码的结果:
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = { class: "app" }
const _hoisted_2 = { key: 1 }
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
export function render(_ctx, _cache) {
const _component_hello = _resolveComponent("hello")
return (_openBlock(), _createBlock("div", _hoisted_1, [
(_ctx.flag)
? _createVNode(_component_hello, { key: 0 })
: (_openBlock(), _createBlock("div", _hoisted_2, [
_createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 /* TEXT */),
_hoisted_3,
_hoisted_4
]))
]))
}
示例的模板是如何转换生成这样的代码的?在 AST 转换后,会执行 generate 函数生成代码:
return generate(ast, extend({}, options, {
prefixIdentifiers
}))
generate 函数的输入就是转换后的 AST 根节点,我们看一下它的实现:
function generate(ast, options = {}) {
// 创建代码生成上下文
const context = createCodegenContext(ast, options);
const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context;
const hasHelpers = ast.helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== 'module';
const genScopeId = scopeId != null && mode === 'module';
// 生成预设代码
if ( mode === 'module') {
genModulePreamble(ast, context, genScopeId);
}
else {
genFunctionPreamble(ast, context);
}
if (!ssr) {
push(`function render(_ctx, _cache) {`);
}
else {
push(`function ssrRender(_ctx, _push, _parent, _attrs) {`);
}
indent();
if (useWithBlock) {
// 处理带 with 的情况,Web 端运行时编译
push(`with (_ctx) {`);
indent();
if (hasHelpers) {
push(`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`);
push(`\n`);
newline();
}
}
// 生成自定义组件声明代码
if (ast.components.length) {
genAssets(ast.components, 'component', context);
if (ast.directives.length || ast.temps > 0) {
newline();
}
}
// 生成自定义指令声明代码
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context);
if (ast.temps > 0) {
newline();
}
}
// 生成临时变量代码
if (ast.temps > 0) {
push(`let `);
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`);
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`);
newline();
}
if (!ssr) {
push(`return `);
}
// 生成创建 VNode 树的表达式
if (ast.codegenNode) {
genNode(ast.codegenNode, context);
}
else {
push(`null`);
}
if (useWithBlock) {
deindent();
push(`}`);
}
deindent();
push(`}`);
return {
ast,
code: context.code,
map: context.map ? context.map.toJSON() : undefined
};
}
generate 主要做五件事情:创建代码生成上下文,生成预设代码,生成渲染函数,生成资源声明代码,以及生成创建 VNode 树的表达式。接下来,我们就依次详细分析这几个流程。
首先,是通过执行 createCodegenContext 创建代码生成上下文,我们来看它的实现:
function createCodegenContext(ast, { mode = 'function', prefixIdentifiers = mode === 'module', sourceMap = false, filename = `template.vue.html`, scopeId = null, optimizeBindings = false, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, ssr = false }) {
const context = {
mode,
prefixIdentifiers,
sourceMap,
filename,
scopeId,
optimizeBindings,
runtimeGlobalName,
runtimeModuleName,
ssr,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
pure: false,
map: undefined,
helper(key) {
return `_${helperNameMap[key]}`
},
push(code) {
context.code += code
},
indent() {
newline(++context.indentLevel)
},
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel
}
else {
newline(--context.indentLevel)
}
},
newline() {
newline(context.indentLevel)
}
}
function newline(n) {
context.push('\n' + ` `.repeat(n))
}
return context
}
这个上下文对象 context 维护了 generate 过程的一些配置,比如 mode、prefixIdentifiers;也维护了 generate 过程的一些状态数据,比如当前生成的代码 code,当前生成代码的缩进 indentLevel 等。
此外,context 还包含了在 generate 过程中可能会调用的一些辅助函数,接下来我会介绍几个常用的方法,它们会在整个代码生成节点过程中经常被用到。
push(code)
,就是在当前的代码 context.code 后追加 code 来更新它的值。
indent()
,它的作用就是增加代码的缩进,它会让上下文维护的代码缩进 context.indentLevel 加 1,内部会执行 newline 方法,添加一个换行符,以及两倍indentLevel 对应的空格来表示缩进的长度。
deindent()
,和 indent 相反,它会减少代码的缩进,让上下文维护的代码缩进 context.indentLevel 减 1,在内部会执行 newline 方法去添加一个换行符,并减少两倍indentLevel 对应的空格的缩进长度。