前言
关于模板编译,从模板字符串template到最终呈现页面,Vue对于这部分的处理过程是:
- 将编写好的 template模板,转换为AST
- 将 AST 转换为JS AST
- 将JS AST转换为render函数
- 执行render函数,生成vnode树
- 经过patch处理,将dom结构呈现在页面上
其中,第一步和第二步我们已经介绍过了,需要的朋友可以再回顾一下:
这一节,我们就接着往下分析,看一看最终的render函数是如何生成的。
generate函数
function baseCompile(template, options = {}) {
...
// 将template转换为AST
// 获取用于操作转换ast的方法
// 将AST转换为JS AST
...
// 将JS AST生成render函数
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
);
}
这一节,我们终于来到了baseCompile函数的最后一步,生成render函数。可以看到,生成render函数的关键就在于generate函数。
function generate(ast, options = {}) {
// 创建代码生成上下文
const context = createCodegenContext(ast, options);
// 从上下文context分解出需要用到的变量及方法
const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context;
// 从ast中取出需要从vue中导入的函数变量数组helpers
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== "module";
const isSetupInlined = false;
const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context;
...
// 生成静态提升代码
genFunctionPreamble(ast, preambleContext);
// 根据是否是ssr模式,决定函数名及内部参数
const functionName = ssr ? `ssrRender` : `render`;
const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];
const signature = args.join(", ");
// 将上面维护好的变量值,使用字符串拼接的方式维护进上下文context的code属性中
push(`function ${functionName}(${signature}) {`);
indent();
if (useWithBlock) {
// 处理带 with 的情况,Web 端运行时编译
push(`with (_ctx) {`);
indent();
if (hasHelpers) {
push(`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue`);
push(`
`);
newline();
}
}
// 如果ast上有组件相关,生成自定义组件声明代码
if (ast.components.length) {
genAssets(ast.components, "component", context);
if (ast.directives.length || ast.temps > 0) {
newline();
}
}
// 如果ast上有指令相关,生成自定义指令声明代码
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(`
`);
newline();
}
if (!ssr) {
push(`return `);
}
// 生成创建 VNode 树的render函数
if (ast.codegenNode) {
genNode(ast.codegenNode, context);
} else {
push(`null`);
}
if (useWithBlock) {
deindent();
push(`}`);
}
deindent();
push(`}`);
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
map: context.map ? context.map.toJSON() : void 0
};
}
上面我们对generate函数做了分析,可以看出,其实经过了之前步骤的处理,ast中的信息已经非常的清晰,在这个函数中我们所要做的就是把render函数给拼出来:
- 首先,创建代码生成上下文,里面包含了拼接过程中用到的工具函数。
- 然后,根据当前模式决定我们维护的render函数的函数名以及参数。
- 之后,根据 AST 中的内容,将对应的代码拼接进上下文context的 code属性 中。
- 最后,将拼接好的 render函数,以及初始的 AST 包装成对象,并 return。
接下来,我们深入看一下generate函数中用到的一些内部方法的具体实现以及他们的作用。
创建代码生成上下文函数createCodegenContext
function createCodegenContext(ast, {
mode = "function",
prefixIdentifiers = mode === "module",
sourceMap = false,
filename = `template.vue.html`,
scopeId = null,
optimizeImports = false,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssrRuntimeModuleName = "vue/server-renderer",
ssr = false,
isTS = false,
inSSR = false
}) {
const context = {
mode,
prefixIdentifiers,
sourceMap,
filename,
scopeId,
optimizeImports,
runtimeGlobalName,
runtimeModuleName,
ssrRuntimeModuleName,
ssr,
isTS,
inSSR,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
pure: false,
map: void 0,
// 获取需要引入的函数方法名
helper(key) {
return `_${helperNameMap[key]}`;
},
// 向上下文context的code属性中拼接字符
push(code, node) {
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;
}
createCodegenContext函数 主要的工作就是创建一个代码生成的上下文context,在context维护了我们需要的一些基本信息以及处理AST的一些工具函数。
- push:主要用于拼接。
- indent 和 deindent:用于处理缩进相关
- newline:用于处理换行。
生成静态提升相关代码genFunctionPreamble
function genFunctionPreamble(ast, context) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName,
ssrRuntimeModuleName
} = context;
const VueBinding = runtimeGlobalName;
const helpers = Array.from(ast.helpers);
if (helpers.length > 0) {
push(`const _Vue = ${VueBinding}
// 如果有静态提升的代码,那么从helpers数组中取出静态提升过程中用到的函数名
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_ELEMENT_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
].filter((helper) => helpers.includes(helper)).map(aliasHelper).join(", ");
push(`const { ${staticHelpers} } = _Vue
`);
}
}
}
// 生成静态提升代码
genHoists(ast.hoists, context);
newline();
push(`return `);
}
genFunctionPreamble函数要做的事情就是生成render函数的前置代码,主要是:
- 如果有需要静态提升的代码,那么从helpers数组中取出静态提升过程中用到的函数名。
- 维护可以静态提升的代码。
这里举个栗子:
<div>
<span> {{x}} </span>
<div>123</div>
</div>
上面的模板代码经过template => AST => JS AST,最终维护好的JS AST是这个样子的:
这里我们可以清晰的看到ast中维护的helpers和hoists两个属性的内容,那么,通过genFunctionPreamble函数我们实际上处理得到的内容是这一部分:
生成render函数主体部分
在生成render函数的主体部分前,还会依次去处理AST上的helpers,components,directives, temps。这里,主要就是判断它们的内容是否为空,不为空则做相应的处理。
最后则是通过genNode函数,来生成render函数的主体部分,我们来看一下genNode函数:
function genNode(node, context) {
if (isString(node)) {
context.push(node);
return;
}
if (isSymbol(node)) {
context.push(context.helper(node));
return;
}
switch (node.type) {
case 1 /* ELEMENT */:
case 9 /* IF */:
case 11 /* FOR */:
assert(
node.codegenNode != null,
`Codegen node is missing for element/if/for node. Apply appropriate transforms first.`
);
genNode(node.codegenNode, context);
break;
case 2 /* TEXT */:
genText(node, context);
break;
case 4 /* SIMPLE_EXPRESSION */:
genExpression(node, context);
break;
case 5 /* INTERPOLATION */:
genInterpolation(node, context);
break;
case 12 /* TEXT_CALL */:
genNode(node.codegenNode, context);
break;
case 8 /* COMPOUND_EXPRESSION */:
genCompoundExpression(node, context);
break;
case 3 /* COMMENT */:
genComment(node, context);
break;
case 13 /* VNODE_CALL */:
genVNodeCall(node, context);
break;
....
}
}
genNode函数是通过遍历AST上的codegenNode属性,根据对应node节点的type值,去调用不同的处理方法。
还是用我们上面的模板为例:
<div>
<span> {{x}} </span>
<div>123</div>
</div>
将ast的codegenNode属性带入genNode函数,我们首先遇到的node对应的type是13,所以相应的就会去调用genVNodeCall函数。
在genVNodeCall函数中,我们又会对该node节点的子节点进行依次处理,最终生成我们需要的render函数。
最后
至此,我们终于是了解了Vue从模板template到render函数的整个编译过程。
整个的过程,虽然繁琐,但是思路还是比较清晰的,这里对整个的过程再做一个总结:
- template编译为AST的逻辑是:通过指针的不断移动,维护剩余的模板字符串,再针对不同的情况具体讨论,调用对应封装好的处理函数,这样一点一点的蚕食模板字符串template,最终将得到的内容拼接为我们最终想要的AST。
- AST 转换为JS AST的逻辑是:遍历第一步中维护的AST节点,生成节点的codegenNode信息,同时做一些静态提升等操作,最终维护出一份信息更加明确的数据出来。
- 生成render函数逻辑是:结合前面步骤维护好的JS AST,遍历AST上的node节点,然后使用封装好的工具函数,通过不断的判断,将render函数一点一点的拼接起来。