上一章咱们详细的分析了Vue3
的模版解析进程,每种不同的节点都对应着不同的解析成果;
而这些解析成果仅仅一个AST
目标,并不能直接用于烘托,所以咱们需要将AST
目标转化为render
函数,而这个进程便是咱们这一章要讲的内容。
baseCompile
回到baseCompile
函数,这个函数是在组件挂载的进程中调用的,组件挂载的进程能够参阅【源码&库】Vue3的组件是怎么挂载的?;
在挂载的进程中,咱们会生成AST
目标,AST
目标怎么生成的能够参阅【源码&库】Vue3的模板转化为AST的进程;
咱们能够从Vue3的模板转化为AST的进程
这篇文章中看到baseCompile
函数的调用进程,baseCompile
函数如下:
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
节点,模板内容如下:
<!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
函数,函数源码如下:
/**
* @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
树的节点并运用转化函数,函数源码如下:
/**
* @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
函数,函数源码如下:
/**
* @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
这个函数的作用是生成函数的前导部分,包含大局变量的引进、静态节点的提高等,函数源码如下:
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
都做了什么,函数源码如下:
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
这个函数的作用是生成子节点代码,函数源码如下:
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
函数源代码,如下:
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转化细节全解析