|
前言
在之前的 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?文章中讲了阶段处理完v-for、v-model等指令后,会生成一棵javascript AST抽象语法树。这篇文章我们来接着讲阶段是如何根据这棵javascript AST抽象语法树生成render函数字符串的,本文中使用的vue版本为。
看个demo
还是一样的套路,我们通过debug一个demo来搞清楚render函数字符串是如何生成的。demo代码如下:- <template>
- <p>{{ msg }}</p>
- </template>
- <script setup lang="ts">
- import { ref } from "vue";
- const msg = ref("hello world");
- </script>
复制代码 上面这个demo很简单,使用p标签渲染一个msg响应式变量,变量的值为"hello world"。我们在浏览器中来看看这个demo生成的render函数是什么样的,代码如下:- import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
- function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
- return _openBlock(), _createElementBlock(
- "p",
- null,
- _toDisplayString($setup.msg),
- 1
- /* TEXT */
- );
- }
复制代码 上面的render函数中使用了两个函数:和。在之前的vue3早已具备抛弃虚拟DOM的能力了文章中我们已经讲过了这两个函数:
- 的作用为初始化一个全局变量数组,用于收集dom树中的所有动态节点。
- 的作用为生成根节点p标签的虚拟DOM,然后将收集到的动态节点数组塞到根节点p标签的属性上。
render函数的生成其实很简单,经过阶段处理后会生成一棵,这棵树的结构和要生成的render函数结构是一模一样的。所以在函数中只需要递归遍历这棵树,进行字符串拼接就可以生成render函数啦!
加我微信heavenyjj0012回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。函数
首先给函数打个断点,函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。
然后启动一个debug终端,在终端中执行(这里是以vite举例)。在浏览器中访问 http://localhost:5173/ ,此时断点就会走到函数中了。在我们这个场景中简化后的函数是下面这样的:- function generate(ast) {
- const context = createCodegenContext();
- const { push, indent, deindent } = context;
- const preambleContext = context;
- genModulePreamble(ast, preambleContext);
- const functionName = `render`;
- const args = ["_ctx", "_cache"];
- args.push("$props", "$setup", "$data", "$options");
- const signature = args.join(", ");
- push(`function ${functionName}(${signature}) {`);
- indent();
- push(`return `);
- genNode(ast.codegenNode, context);
- deindent();
- push(`}`);
- return {
- ast,
- code: context.code,
- };
- }
复制代码 中主要分为四部分:
- 生成上下文对象。
- 执行函数生成:
- import { xxx } from "vue";
复制代码 - 生成render函数中的函数名称和参数,也就是
- function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
复制代码 - 生成render函数中return的内容
上下文对象上下文对象是执行函数生成的,将断点走进函数。简化后的代码如下:- function createCodegenContext() {
- const context = {
- code: ``,
- indentLevel: 0,
- 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;
- }
复制代码 为了代码具有较强的可读性,我们一般都会使用换行和锁进。上下文中的这些属性和方法作用就是为了生成具有较强可读性的render函数。属性:当前生成的render函数字符串。
- 属性:当前的锁进级别,每个级别对应两个空格的锁进。
- 方法:返回render函数中使用到的vue包中export导出的函数名称,比如返回、等函数
- 方法:向当前的render函数字符串后插入字符串code。
- 方法:插入换行符,并且增加一个锁进。
- 方法:减少一个锁进,或者插入一个换行符并且减少一个锁进。
- 方法:插入换行符。
生成我们接着来看函数中的第二部分,生成。将断点走进函数,在我们这个场景中简化后的函数代码如下:- function genModulePreamble(ast, context) {
- const { push, newline, runtimeModuleName } = context;
- if (ast.helpers.size) {
- const helpers = Array.from(ast.helpers);
- push(
- `import { ${helpers
- .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
- .join(", ")} } from ${JSON.stringify(runtimeModuleName)}
- `,
- -1 /* End */
- );
- }
- genHoists(ast.hoists, context);
- newline();
- push(`export `);
- }
复制代码 其中的是在阶段收集的需要从vue中import导入的函数,无需将vue中所有的函数都import导入。在debug终端看看数组中的值如下图:
从上图中可以看到需要从vue中import导入、、这三个函数。
在执行方法之前我们先来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时生成的render函数字符串还是一个空字符串,执行完push方法后,我们来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数中已经有了了。
这里执行的函数就是前面 搞懂 Vue 3 编译优化:静态提升的秘密文章中讲过的静态提升的入口。
生成render函数中的函数名称和参数
执行完函数后,已经生成了一条了。我们接着来看函数中render函数的函数名称和参数是如何生成的,代码如下:- const functionName = `render`;
- const args = ["_ctx", "_cache"];
- args.push("$props", "$setup", "$data", "$options");
- const signature = args.join(", ");
- push(`function ${functionName}(${signature}) {`);
复制代码 上面的代码很简单,都是执行方法向render函数中添加code字符串,其中数组就是render函数中的参数。我们在来看看执行完上面这块代码后的render函数字符串是什么样的,如下图:
从上图中可以看到此时已经生成了render函数中的函数名称和参数了。
生成render函数中return的内容
接着来看函数中最后一块代码,如下:- indent();
- push(`return `);
- genNode(ast.codegenNode, context);
复制代码 首先调用方法插入一个换行符并且增加一个锁进,然后执行方法添加一个字符串。
接着以根节点的属性为参数执行函数生成return中的内容,在我们这个场景中函数简化后的代码如下:- function genNode(node, context) {
- switch (node.type) {
- case NodeTypes.SIMPLE_EXPRESSION:
- genExpression(node, context)
- break
- case NodeTypes.INTERPOLATION:
- genInterpolation(node, context);
- break;
- case NodeTypes.VNODE_CALL:
- genVNodeCall(node, context);
- break;
- }
- }
复制代码 这里涉及到、和三种AST抽象语法树node节点类型:
- :表示当前节点是双大括号节点,我们这个demo中就是:这个文本节点。
- :表示当前节点是简单表达式节点,在我们这个demo中就是双大括号节点中的更里层节点
- :表示当前节点是虚拟节点,比如我们这里第一次调用函数传入的(根节点的属性)就是虚拟节点。
函数
由于当前节点是虚拟节点,第一次进入函数时会执行函数。在我们这个场景中简化后的函数代码如下:- const OPEN_BLOCK = Symbol(`openBlock`);
- const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);
- function genVNodeCall(node, context) {
- const { push, helper } = context;
- const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
- if (isBlock) {
- push(`(${helper(OPEN_BLOCK)}(${``}), `);
- }
- const callHelper = CREATE_ELEMENT_BLOCK;
- push(helper(callHelper) + `(`, -2 /* None */, node);
- genNodeList(
- // 将参数中的undefined转换成null
- genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
- context
- );
- push(`)`);
- if (isBlock) {
- push(`)`);
- }
- }
复制代码 首先判断当前节点是不是block节点,由于此时的node为根节点,所以为true。将断点走进方法,我们来看看返回值是什么。方法的代码如下:- const helperNameMap = {
- [OPEN_BLOCK]: `openBlock`,
- [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
- [TO_DISPLAY_STRING]: `toDisplayString`,
- // ...省略
- };
- helper(key) {
- return `_${helperNameMap[key]}`;
- }
复制代码 方法中的代码很简单,这里的返回的就是。
将断点走到第一个方法,代码如下:- push(`(${helper(OPEN_BLOCK)}(${``}), `);
复制代码 执行完这个方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个函数的调用。
将断点走到第二个方法,代码如下:- const callHelper = CREATE_ELEMENT_BLOCK;
- push(helper(callHelper) + `(`, -2 /* None */, node);
复制代码 同理方法返回的是,执行完这个方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个函数的调用。
继续将断点走到部分,代码如下:- genNodeList(
- genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
- context
- );
复制代码 其中的函数功能很简单,将参数中的转换成。比如此时的就是,经过函数处理后传给函数的就是。函数
继续将断点走进函数,在我们这个场景中简化后的代码如下:- function genNodeList(nodes, context, multilines = false, comma = true) {
- const { push } = context;
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i];
- if (shared.isString(node)) {
- push(node);
- } else {
- genNode(node, context);
- }
- if (i < nodes.length - 1) {
- comma && push(", ");
- }
- }
- }
复制代码 我们先来看看此时的参数,如下图:
这里的就是调用函数时传的数组:- [tag, props, children, patchFlag, dynamicProps]
复制代码 ,只是将数组中的转换成了。
- 数组中的第一项为字符串p,表示当前节点是p标签。
- 由于当前p标签没有props,所以第二项为null的字符串。
- 第三项为p标签子节点:{{msg}}
- 第四项也是一个字符串,标记当前节点是否是动态节点。
在讲函数之前,我们先来看一下如何使用函数生成一个标签的虚拟DOM节点。根据vue官网的介绍,函数定义如下:- // 完整参数签名
- function h(
- type: string | Component,
- props?: object | null,
- children?: Children | Slot | Slots
- ): VNode
复制代码 函数接收的第一个参数是标签名称或者一个组件,第二个参数是props对象或者null,第三个参数是子节点。
所以我们要使用函数生成demo中的p标签虚拟DOM节点代码如下:函数生成虚拟DOM实际就是调用的函数,而我们这里的函数生成虚拟DOM也是调用的函数。两者的区别是函数多接收一些参数,比如和。
现在我想你应该已经反应过来了,为什么调用函数时传入的第一个参数为:- [tag, props, children, patchFlag, dynamicProps]
复制代码 。这个数组的顺序就是调用函数时传入的参数顺序。
所以在中会遍历数组生成调用函数需要传入的参数。
先来看第一个参数,这里的值为字符串"p"。所以在for循环中会执行,生成调用函数的第一个参数"p"。在debug终端看看此时的render函数,如下图:
从上图中可以看到函数的第一个参数"p"
接着来看数组中的第二个参数:,由于p标签中没有属性。所以第二个参数的值为字符串"null",在for循环中同样会执行,生成调用函数的第二个参数"null"。在debug终端看看此时的render函数,如下图:
从上图中可以看到函数的第二个参数接着来看数组中的第三个参数:,由于是一个对象,所以以当前children节点作为参数执行函数。
这个函数前面已经执行过一次了,当时是以根节点的属性作为参数执行的。回顾一下函数的代码,如下:- function genNode(node, context) {
- switch (node.type) {
- case NodeTypes.SIMPLE_EXPRESSION:
- genExpression(node, context)
- break
- case NodeTypes.INTERPOLATION:
- genInterpolation(node, context);
- break;
- case NodeTypes.VNODE_CALL:
- genVNodeCall(node, context);
- break;
- }
- }
复制代码 前面我们讲过了类型表示当前节点是双大括号节点,而我们这次执行函数传入的p标签children,刚好就是{{msg}}双大括号节点。所以代码会走到函数中。函数
将断点走进函数中,代码如下:- function genInterpolation(node, context) {
- const { push, helper } = context;
- push(`${helper(TO_DISPLAY_STRING)}(`);
- genNode(node.content, context);
- push(`)`);
- }
复制代码 首先会执行方法向render函数中插入一个函数调用,在debug终端看看执行完这个方法后的render函数,如下图:
从上图中可以看到此时函数的第三个参数只生成了一半,调用函数传入的参数还没生成。
接着会以作为参数执行- genNode(node.content, context);
复制代码 生成函数的参数,此时代码又走回了函数。
将断点再次走进函数,看看此时的node是什么样的,如下图:
从上图中可以看到此时的node节点是一个简单表达式节点,表达式为:。所以代码会走进函数。函数
接着将断点走进函数中,函数中的代码如下:- function genExpression(node, context) {
- const { content, isStatic } = node;
- context.push(
- isStatic ? JSON.stringify(content) : content,
- -3 /* Unknown */,
- node
- );
- }
复制代码 由于当前的变量是一个响应式变量,所以为。所以会执行方法,将插入到render函数中。
执行完方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数基本已经生成了,剩下的就是调用方法生成各个函数的右括号")"和右花括号"}"。将断点逐层走出,直到函数中。代码如下:- function generate(ast) {
- // ...省略
- genNode(ast.codegenNode, context);
- deindent();
- push(`}`);
- return {
- ast,
- code: context.code,
- };
- }
复制代码 执行完最后一个方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数终于生成啦!
总结
这是我画的我们这个场景中生成render函数的流程图:
- 执行函数生成:
- import { xxx } from "vue";
复制代码 - 简单字符串拼接生成render函数中的函数名称和参数,也就是
- function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
复制代码 - 以根节点的属性为参数调用函数生成render函数中return的内容。
- 此时传入的是虚拟节点,执行函数生成
- return _openBlock(), _createElementBlock(
复制代码 和调用函数,生成函数的参数。
- 处理p标签的标签名和,生成函数的第一个和第二个参数。此时render函数return的内容为:
- return _openBlock(), _createElementBlock("p", null
复制代码 - 处理p标签的children也就是节点,再次调用函数。此时node节点类型为双大括号节点,调用函数。
- 在函数中会先调用方法,此时的render函数return的内容为:
- return _openBlock(), _createElementBlock("p", null, _toDisplayString(
复制代码 。然后以为参数再次调用函数。
- 为,是一个简单表达式节点,所以在函数中会调用函数。执行完函数后,此时的render函数return的内容为:
- return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg
复制代码 - 调用push方法生成各个函数的右括号")"和右花括号"}",生成最终的render函数
到此这篇关于原来 Vue 3 的 generate 是这样生成 render 函数的的文章就介绍到这了,更多相关原来vue3中template使用ref无需.value是因为这个内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/javascript/3223916bn.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|