上一章我们详细的分析了Vue3
的模版解析过程,每种不同的节点都对应着不同的解析结果;
而这些解析结果只是一个AST
对象,并不能直接用于渲染,所以我们需要将AST
对象转换为render
函数,而这个过程就是我们这一章要讲的内容。
baseCompile
回到baseCompile
函数,这个函数是在组件挂载的过程中调用的,组件挂载的过程可以参考【源码&库】Vue3的组件是如何挂载的?;
在挂载的过程中,我们会生成AST
对象,AST
对象如何生成的可以参考【源码&库】Vue3的模板转换为AST的过程;
我们可以从Vue3的模板转换为AST的过程
这篇文章中看到baseCompile
函数的调用过程,baseCompile
函数如下:
js
function baseCompile(template, options = {}) {
const onError = options.onError || defaultOnError;
const isModuleMode = options.mode === "module";
{
if (options.prefixIdentifiers === true) {
onError(createCompilerError(47));
} else if (isModuleMode) {
onError(createCompilerError(48));
}
}
const prefixIdentifiers = false;
if (options.cacheHandlers) {
onError(createCompilerError(49));
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(50));
}
// 这一步就是我们上两章讲的模版解析过程
const ast = isString(template) ? baseParse(template, options) : template;
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
// 下面的 transform 和 generate 函数就是我们这一章要讲的内容
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...options.nodeTransforms || []
// user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {}
// user transforms
)
})
);
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
);
}
由于之前并没有放出baseCompile
函数的所有源码,这里放出来完整源码方便大家有一个完整的认识;
同时也是让大家知道我花了两章的内容去阅读的源码,其实只是baseCompile
函数的一次函数调用;
从这里也知道这个函数有多核心,而且源码阅读的过程也是非常枯燥且痛苦的,但是阅读完成之后会发现自己的收获是非常大的;
不多说,dddd(懂的都懂)
,我们继续往下看;
这一章我们将模板内容做稍微复杂一点,这样就能看到更多的AST
节点,模板内容如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id='app'>
<h1>Title</h1>
<my-component :message="message"></my-component>
<div @click="handleClick">{{ message }}</div>
</div>
</body>
<script src="./vue.global.js"></script>
<script>
const {createApp, h} = Vue;
const app = createApp({
data() {
return {
message: 'hello vue'
}
},
methods: {
handleClick() {
this.message = 'hello vue3';
}
}
});
const MyComponent = {
props: ['message'],
render() {
return h('div', this.message);
}
};
app.component('MyComponent', MyComponent);
debugger;
app.mount('#app');
</script>
</html>
如果想要深入的话可以更复杂化这个模板,例如增加注释节点、v-if、v-for等指令、v-model、插槽等等;
transform
在transform
函数之前,还有一个getBaseTransformPreset
函数,这个函数的作用是获取nodeTransforms
和directiveTransforms
;
这个函数主要是用于返回一个预设的转换函数,用于解析v-if
、v-for
、v-on
等指令,这里就不详细讲解了,大家可以自行阅读源码;
我们直接来看transform
函数,这个函数的作用是将AST
对象转换为render
函数,函数源码如下:
js
/**
* @param root ast对象
* @param options 编译的配置项,包括nodeTransforms、directiveTransforms等
*/
function transform(root, options) {
// 创建一个 AST 转换上下文对象,这个上下文对象包含了 AST 树转换过程中所需的信息和状态
const context = createTransformContext(root, options);
// 遍历 AST 树的节点并应用转换函数
traverseNode(root, context);
// 静态节点提升
if (options.hoistStatic) {
hoistStatic(root, context);
}
// 生成根节点的代码
if (!options.ssr) {
createRootCodegen(root, context);
}
// 收集 ast 上下文 helpers 函数的 key 值
root.helpers = /* @__PURE__ */ new Set([...context.helpers.keys()]);
// 收集 ast 上下文的组件
root.components = [...context.components];
// 收集 ast 上下文的指令
root.directives = [...context.directives];
// 收集 ast 上下文的导入
root.imports = context.imports;
// 收集 ast 上下文的静态节点提升数组
root.hoists = context.hoists;
// 收集 ast 上下文的临时变量
root.temps = context.temps;
// 收集 ast 上下文的缓存节点数组
root.cached = context.cached;
}
这里的createTransformContext
函数主要是创建一个AST
转换上下文对象,总的来说就是一个对象,这个对象上面有很多属性和方法;
由于太多就不贴源码了,感兴趣的同学可以自行阅读源码,这里不做过多的讲解;
接下来我们看traverseNode
函数,这个函数的作用是遍历AST
树的节点并应用转换函数,函数源码如下:
js
/**
* @param node ast对象
* @param context 转换上下文对象
*/
function traverseNode(node, context) {
// 将转换上下文的当前节点设置为当前 ast 节点,用于表示当前正在转换的节点
context.currentNode = node;
// 从上下文对象中获取节点转换函数的数组 nodeTransforms
const {nodeTransforms} = context;
// 初始化一个数组 exitFns 用于存储节点转换时的退出函数
const exitFns = [];
// 遍历执行节点转换函数
for (let i2 = 0; i2 < nodeTransforms.length; i2++) {
// 执行节点转换函数,并将返回值赋值给 onExit
// 这里执行的通常都是一些转换函数,比如处理 v-if、v-for、v-on 等指令的转换函数
// 返回的 onExit 通常是一个函数,用于在节点转换结束后执行
const onExit = nodeTransforms[i2](node, context);
// 如果有 onExit 函数,则将 onExit 函数添加到 exitFns 数组中
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}
// 如果当前节点已经被替换了,则直接退出遍历
// 这里通常表示当前节点被移除了,比如 v-if 指令的条件不满足时,会将当前节点移除
// 所以就没有必要再遍历当前节点的子节点了
if (!context.currentNode) {
return;
} else {
node = context.currentNode;
}
}
// 根据当前节点的类型,执行不同的遍历函数
switch (node.type) {
// 3 是注释节点
case 3:
if (!context.ssr) {
context.helper(CREATE_COMMENT);
}
break;
// 5 是插值表达式节点
case 5:
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING);
}
break;
// 9 是条件节点
case 9:
for (let i2 = 0; i2 < node.branches.length; i2++) {
traverseNode(node.branches[i2], context);
}
break;
case 10: // 10 是元素节点
case 11: // 11 是组件节点
case 1: // 1 是元素节点
case 0: // 0 是根节点
traverseChildren(node, context);
break;
}
// 确保最终状态是当前节点
context.currentNode = node;
// 执行 exitFns 数组中的所有函数
// 因为组件会有子节点,有些操作是在子节点转换完成后才能执行
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
这里的traverseNode
函数主要是遍历AST
树的节点并应用转换函数,执行这一部分之后,AST
的结构会发生很大的变化,可以简单看看对比:
generate
转换完成之后就该调用generate
函数了,这个函数的作用是将AST
对象转换为render
函数源代码,动态生成render
函数,函数源码如下:
js
/**
* @param ast ast对象
* @param options 编译的配置项,包括nodeTransforms、directiveTransforms等
*/
function generate(ast, options = {}) {
// 创建了代码生成上下文,包含了生成代码所需的各种信息和工具函数
// 和其他的上下文对象一样,这个上下文对象也是一个对象,上面有很多属性和方法,就不贴源码了
const context = createCodegenContext(ast, options);
// 是否通过指定上下文进行创建
if (options.onContextCreated)
options.onContextCreated(context);
// 解构出上下文对象中的以下属性
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context;
// 根据上面的截图我们知道 helpers 里面存放的是一些 Symbol 类型的变量
// 这个会在下面有使用
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
// 根据选项和模式,确定是否使用 with 块。
const useWithBlock = !prefixIdentifiers && mode !== "module";
// 用于标识设置函数是否已经内联
// 这里是因为构建之后产生的代码,所以固定为 false,源码会根据情况进行判断
const isSetupInlined = false;
const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context;
// 生成函数的前导部分,包括可能的 with 块、帮助函数的引入等
genFunctionPreamble(ast, preambleContext);
// 根据是否为服务端渲染,确定函数的名称
const functionName = ssr ? `ssrRender` : `render`;
// 根据是否为服务端渲染,确定函数的参数列表
const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];
// 生成函数的开头部分,包括函数名、参数列表等
// 会生成:function render(_ctx, _cache) {
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
// 增加缩进
indent();
// 生成 with 块
if (useWithBlock) {
push(`with (_ctx) {`);
indent();
if (hasHelpers) {
// 如果有 helpers,则生成 helpers 的引入
// 根据上面的截图,我们可以知道生成的内容是:
// const { createElementVNode, resolveComponent, ... } = _Vue
push(`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue`);
// 添加换行
push(`
`);
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(`
`);
newline();
}
// 不是服务端渲染,生成 return 语句
if (!ssr) {
push(`return `);
}
// 生成子节点代码
if (ast.codegenNode) {
genNode(ast.codegenNode, context);
} else {
push(`null`);
}
// 如果使用 with 块,减少缩进并生成块的结束
if (useWithBlock) {
deindent();
push(`}`);
}
// 减少缩进并生成函数的结束
deindent();
push(`}`);
// 返回一个对象,包含生成的 AST、生成的代码、前导代码(如果设置函数已内联)、以及生成的 Source Map。
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? context.map.toJSON() : void 0
};
}
这个函数本身其实已经很复杂了,这里光看代码上面的注释可能还是不太好理解,我就简单的给再梳理一下:
- 首先是创建了一个代码生成上下文,这个上下文对象中有一个
code
属性,这个属性就是最终生成的render
函数源代码; - 执行
genFunctionPreamble
函数生成函数的前导部分,包括全局变量的引入、静态节点的提升等; - 生成函数的开头部分,包括函数名、参数列表、
with
块等; - 生成组件、指令、临时变量代码;
- 生成
return
语句之后再生成子节点代码; - 生成子节点代码;
- 生成函数的结束;
这里注意的是第5
步,因为render
函数返回的是一个VNode
节点,所以在生成return
语句之后再生成子节点代码,并返回创建子节点的创建代码;
这里我们推荐深挖的就是genFunctionPreamble
和genNode
这两个函数,这两个函数的作用是生成函数的前导部分和生成子节点代码;
genFunctionPreamble
这个函数的作用是生成函数的前导部分,包括全局变量的引入、静态节点的提升等,函数源码如下:
js
function genFunctionPreamble(ast, context) {
// 解构出上下文对象中的以下属性
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName,
ssrRuntimeModuleName
} = context;
// 获取运行在全局的 Vue 的名称,这里就是 Vue
const VueBinding = runtimeGlobalName;
// 辅助函数的名称
const helpers = Array.from(ast.helpers);
if (helpers.length > 0) {
// 这里就相当于是一个别名,后续的代码全都会写死用这个别名获取全局的 Vue
// const _Vue = Vue
push(`const _Vue = ${VueBinding}
`);
// 如果有静态节点提升,则生成静态节点提升的需要用到的辅助函数
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(", ");
// 通过别名获取辅助函数,并生成代码,这里的 staticHelpers 会生成一个解构赋值的键值对代码
// const { createVNode: _createVNode, ... } = _Vue
push(`const { ${staticHelpers} } = _Vue
`);
}
}
// 生成静态节点提升的代码
genHoists(ast.hoists, context);
// 换行
newline();
// 生成 return 语句
push(`return `);
}
这里的genFunctionPreamble
函数内容并不复杂,主要是生成函数的前导部分,包括全局变量的引入、静态节点的提升等;
接下来看看genHoists
都做了什么,函数源码如下:
js
function genHoists(hoists, context) {
// 如果没有静态节点提升,则直接返回
if (!hoists.length) {
return;
}
// 将代码生成上下文中的 pure 属性设置为 true,表示生成的代码是纯粹的表达式,没有副作用
context.pure = true;
const { push, newline, helper, scopeId, mode } = context;
newline();
// 生成静态节点提升的代码
for (let i = 0; i < hoists.length; i++) {
// 获取静态节点提升的节点
const exp = hoists[i];
if (exp) {
// 使用索引命名保持变量名唯一性
push(
`const _hoisted_${i + 1} = ${``}`
);
// 使用 genNode 生成生成节点创建代码
genNode(exp, context);
newline();
}
}
// 重置 pure 属性为 false,表示生成的代码可能有副作用
context.pure = false;
}
可以看到这里核心也是使用genNode
来生成节点创建代码,我们继续;
genNode
这个函数的作用是生成子节点代码,函数源码如下:
js
function genNode(node, context) {
// 如果是 string 类型,则直接作为代码
if (isString(node)) {
context.push(node);
return;
}
// 如果是 symbol 类型,则使用辅助函数生成代码
if (isSymbol(node)) {
context.push(context.helper(node));
return;
}
// 根据节点类型,执行不同的生成函数
switch (node.type) {
case 1: // 元素节点
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: // 文本节点
genText(node, context);
break;
case 4: // 表达式节点
genExpression(node, context);
break;
case 5: // 插值节点
genInterpolation(node, context);
break;
case 12: // fragment 节点
genNode(node.codegenNode, context);
break;
case 8: // 复合表达式节点
genCompoundExpression(node, context);
break;
case 3: // 注释节点
genComment(node, context);
break;
case 13: // 生成 createVNode 的调用
genVNodeCall(node, context);
break;
case 14: // 生成普通函数调用
genCallExpression(node, context);
break;
case 15: // 生成对象表达式
genObjectExpression(node, context);
break;
case 17: // 生成数组表达式
genArrayExpression(node, context);
break;
case 18: // 生成函数表达式
genFunctionExpression(node, context);
break;
case 19: // 生成条件表达式
genConditionalExpression(node, context);
break;
case 20: // 生成缓存表达式
genCacheExpression(node, context);
break;
case 21: // 生成节点列表
genNodeList(node.body, context, true, false);
break;
case 22:
break;
case 23:
break;
case 24:
break;
case 25:
break;
case 26:
break;
case 10:
break;
default:
{
assert(false, `unhandled codegen node type: ${node.type}`);
const exhaustiveCheck = node;
return exhaustiveCheck;
}
}
}
这里的genNode
函数主要是根据节点类型,执行不同的生成函数,可以看到具体的函数有很多,由于篇幅以及内容的复杂性,这里就留到下一章了;
最后我们可以看看生成的render
函数源代码,如下:
js
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Title", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
const _component_my_component = _resolveComponent("my-component")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_createVNode(_component_my_component, { message: message }, null, 8 /* PROPS */, ["message"]),
_createElementVNode("div", { onClick: handleClick }, _toDisplayString(message), 9 /* TEXT, PROPS */, _hoisted_2)
], 64 /* STABLE_FRAGMENT */))
}
}
这里只需要将这段源码交给new Function
执行,就可以生成一个函数代码,new Function
的参数就是函数体的代码,所以上面的代码是直接写了return
的,小知识点;
总结
这一章我们主要是讲解了AST
对象转换为render
函数的过程,这个过程主要是通过transform
和generate
函数来实现的;
tansform
函数的作用是对AST
树进行转换,并提供对应的辅助函数,用于后续的代码生成;
generate
函数的作用是将AST
对象转换为render
函数源代码,动态生成render
函数;
最后将生成的代码交给new Function
进行生成一个可执行的函数,这个函数就是render
函数;
历史章节
- 【源码&库】跟着 Vue3 学习前端模块化
- 【源码&库】在调用 createApp 时,Vue 为我们做了那些工作?
- 【源码&库】细数 Vue3 的实例方法和属性背后的故事
- 【源码&库】Vue3 中的 nextTick 魔法背后的原理
- 【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析
- 【源码&库】跟着 Vue3 的源码学习 reactive 背后的实现原理
- 【源码&库】 Vue3 的依赖收集,这里的依赖指代的是什么?
- 【源码&库】 Vue3 的依赖收集和依赖触发是如何工作的
- 【源码&库】 Vue3 的组件是如何挂载的?
- 【源码&库】 Vue3 的组件是如何更新的?
- 【源码&库】 Vue3 的组件更新核心算法
- 【源码&库】 Vue3 的虚拟DOM生成规则
- 【源码&库】 Vue3 全局组件注册如何实现
- 【源码&库】Vue3的模板转换为AST的过程
- 【源码&库】Vue3的AST转换细节全解析