前言
在之前的 文章中讲了transform
阶段处理完v-for、v-model等指令后,会生成一棵javascript ast抽象语法树。这篇文章我们来接着讲generate
阶段是如何根据这棵javascript ast抽象语法树生成render函数字符串的,本文中使用的vue版本为3.4.19
。
看个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函数中使用了两个函数:openblock
和createelementblock
。在之前的 vue3早已具备抛弃虚拟dom的能力了文章中我们已经讲过了这两个函数:
openblock
的作用为初始化一个全局变量currentblock
数组,用于收集dom树中的所有动态节点。createelementblock
的作用为生成根节点p标签的虚拟dom,然后将收集到的动态节点数组currentblock
塞到根节点p标签的dynamicchildren
属性上。
render函数的生成其实很简单,经过transform
阶段处理后会生成一棵javascript ast抽象语法树
,这棵树的结构和要生成的render函数结构是一模一样的。所以在generate
函数中只需要递归遍历这棵树,进行字符串拼接就可以生成render函数啦!
加我微信heavenyjj0012回复「666」,免费领取欧阳研究vue源码过程中收集的源码资料,欧阳写文章有时也会参考这些资料。同时让你的朋友圈多一位对vue有深入理解的人。
generate
函数
首先给generate
函数打个断点,generate
函数在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。
然后启动一个debug终端,在终端中执行yarn dev
(这里是以vite举例)。在浏览器中访问 http://localhost:5173/ ,此时断点就会走到generate
函数中了。在我们这个场景中简化后的generate
函数是下面这样的:
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, }; }
generate
中主要分为四部分:
- 生成
context
上下文对象。 - 执行
genmodulepreamble
函数生成:import { xxx } from "vue";
- 生成render函数中的函数名称和参数,也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
- 生成render函数中return的内容
context
上下文对象
context
上下文对象是执行createcodegencontext
函数生成的,将断点走进createcodegencontext
函数。简化后的代码如下:
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; }
为了代码具有较强的可读性,我们一般都会使用换行和锁进。context
上下文中的这些属性和方法作用就是为了生成具有较强可读性的render函数。
code
属性:当前生成的render函数字符串。
indentlevel
属性:当前的锁进级别,每个级别对应两个空格的锁进。helper
方法:返回render函数中使用到的vue包中export导出的函数名称,比如返回openblock
、createelementblock
等函数push
方法:向当前的render函数字符串后插入字符串code。indent
方法:插入换行符,并且增加一个锁进。deindent
方法:减少一个锁进,或者插入一个换行符并且减少一个锁进。newline
方法:插入换行符。
生成import {xxx} from "vue"
我们接着来看generate
函数中的第二部分,生成import {xxx} from "vue"
。将断点走进genmodulepreamble
函数,在我们这个场景中简化后的genmodulepreamble
函数代码如下:
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 `); }
其中的ast.helpers
是在transform
阶段收集的需要从vue中import导入的函数,无需将vue中所有的函数都import导入。在debug终端看看helpers
数组中的值如下图:
从上图中可以看到需要从vue中import导入todisplaystring
、openblock
、createelementblock
这三个函数。
在执行push
方法之前我们先来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时生成的render函数字符串还是一个空字符串,执行完push方法后,我们来看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数中已经有了import {xxx} from "vue"
了。
这里执行的genhoists
函数就是前面 搞懂 vue 3 编译优化:静态提升的秘密文章中讲过的静态提升的入口。
生成render函数中的函数名称和参数
执行完genmodulepreamble
函数后,已经生成了一条import {xxx} from "vue"
了。我们接着来看generate
函数中render函数的函数名称和参数是如何生成的,代码如下:
const functionname = `render`; const args = ["_ctx", "_cache"]; args.push("$props", "$setup", "$data", "$options"); const signature = args.join(", "); push(`function ${functionname}(${signature}) {`);
上面的代码很简单,都是执行push
方法向render函数中添加code字符串,其中args
数组就是render函数中的参数。我们在来看看执行完上面这块代码后的render函数字符串是什么样的,如下图:
从上图中可以看到此时已经生成了render函数中的函数名称和参数了。
生成render函数中return的内容
接着来看generate
函数中最后一块代码,如下:
indent(); push(`return `); gennode(ast.codegennode, context);
首先调用indent
方法插入一个换行符并且增加一个锁进,然后执行push
方法添加一个return
字符串。
接着以根节点的codegennode
属性为参数执行gennode
函数生成return中的内容,在我们这个场景中gennode
函数简化后的代码如下:
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; } }
这里涉及到simple_expression
、interpolation
和vnode_call
三种ast抽象语法树node节点类型:
interpolation
:表示当前节点是双大括号节点,我们这个demo中就是:{{msg}}
这个文本节点。simple_expression
:表示当前节点是简单表达式节点,在我们这个demo中就是双大括号节点{{msg}}
中的更里层节点msg
vnode_call
:表示当前节点是虚拟节点,比如我们这里第一次调用gennode
函数传入的ast.codegennode
(根节点的codegennode
属性)就是虚拟节点。
genvnodecall
函数
由于当前节点是虚拟节点,第一次进入gennode
函数时会执行genvnodecall
函数。在我们这个场景中简化后的genvnodecall
函数代码如下:
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为根节点,所以isblock
为true。将断点走进helper
方法,我们来看看helper(open_block)
返回值是什么。helper
方法的代码如下:
const helpernamemap = { [open_block]: `openblock`, [create_element_block]: `createelementblock`, [to_display_string]: `todisplaystring`, // ...省略 }; helper(key) { return `_${helpernamemap[key]}`; }
helper
方法中的代码很简单,这里的helper(open_block)
返回的就是_openblock
。
将断点走到第一个push
方法,代码如下:
push(`(${helper(open_block)}(${``}), `);
执行完这个push
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个_openblock
函数的调用。
将断点走到第二个push
方法,代码如下:
const callhelper = create_element_block; push(helper(callhelper) + `(`, -2 /* none */, node);
同理helper(callhelper)
方法返回的是_createelementblock
,执行完这个push
方法后在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到,此时render函数中增加了一个_createelementblock
函数的调用。
继续将断点走到gennodelist
部分,代码如下:
gennodelist( gennullableargs([tag, props, children, patchflag, dynamicprops]), context );
其中的gennullableargs
函数功能很简单,将参数中的undefined
转换成null
。比如此时的props
就是undefined
,经过gennullableargs
函数处理后传给gennodelist
函数的props
就是null
。
gennodelist
函数
继续将断点走进gennodelist
函数,在我们这个场景中简化后的代码如下:
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(", "); } } }
我们先来看看此时的nodes
参数,如下图:
这里的nodes
就是调用gennodelist
函数时传的数组:[tag, props, children, patchflag, dynamicprops]
,只是将数组中的undefined
转换成了null
。
nodes
数组中的第一项为字符串p,表示当前节点是p标签。- 由于当前p标签没有props,所以第二项为null的字符串。
- 第三项为p标签子节点:{{msg}}
- 第四项也是一个字符串,标记当前节点是否是动态节点。
在讲gennodelist
函数之前,我们先来看一下如何使用h
函数生成一个<p>{{ msg }}</p>
标签的虚拟dom节点。根据vue官网的介绍,h
函数定义如下:
// 完整参数签名 function h( type: string | component, props?: object | null, children?: children | slot | slots ): vnode
h
函数接收的第一个参数是标签名称或者一个组件,第二个参数是props对象或者null,第三个参数是子节点。
所以我们要使用h
函数生成demo中的p标签虚拟dom节点代码如下:
h("p", null, msg)
h
函数生成虚拟dom实际就是调用的createbasevnode
函数,而我们这里的createelementblock
函数生成虚拟dom也是调用的createbasevnode
函数。两者的区别是createelementblock
函数多接收一些参数,比如patchflag
和dynamicprops
。
现在我想你应该已经反应过来了,为什么调用gennodelist
函数时传入的第一个参数nodes
为:[tag, props, children, patchflag, dynamicprops]
。这个数组的顺序就是调用createelementblock
函数时传入的参数顺序。
所以在gennodelist
中会遍历nodes
数组生成调用createelementblock
函数需要传入的参数。
先来看第一个参数tag
,这里tag
的值为字符串"p"。所以在for循环中会执行push(node)
,生成调用createelementblock
函数的第一个参数"p"。在debug终端看看此时的render函数,如下图:
从上图中可以看到createelementblock
函数的第一个参数"p"
接着来看nodes
数组中的第二个参数:props
,由于p标签中没有props
属性。所以第二个参数props
的值为字符串"null",在for循环中同样会执行push(node)
,生成调用createelementblock
函数的第二个参数"null"。在debug终端看看此时的render函数,如下图:
从上图中可以看到createelementblock
函数的第二个参数null
接着来看nodes
数组中的第三个参数:children
,由于children
是一个对象,所以以当前children节点作为参数执行gennode
函数。
这个gennode
函数前面已经执行过一次了,当时是以根节点的codegennode
属性作为参数执行的。回顾一下gennode
函数的代码,如下:
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; } }
前面我们讲过了nodetypes.interpolation
类型表示当前节点是双大括号节点,而我们这次执行gennode
函数传入的p标签children,刚好就是{{msg}}双大括号节点。所以代码会走到geninterpolation
函数中。
geninterpolation
函数
将断点走进geninterpolation
函数中,geninterpolation
代码如下:
function geninterpolation(node, context) { const { push, helper } = context; push(`${helper(to_display_string)}(`); gennode(node.content, context); push(`)`); }
首先会执行push
方法向render函数中插入一个_todisplaystring
函数调用,在debug终端看看执行完这个push
方法后的render函数,如下图:
从上图中可以看到此时createelementblock
函数的第三个参数只生成了一半,调用_todisplaystring
函数传入的参数还没生成。
接着会以node.content
作为参数执行gennode(node.content, context);
生成_todisplaystring
函数的参数,此时代码又走回了gennode
函数。
将断点再次走进gennode
函数,看看此时的node是什么样的,如下图:
从上图中可以看到此时的node节点是一个简单表达式节点,表达式为:$setup.msg
。所以代码会走进genexpression
函数。
genexpression
函数
接着将断点走进genexpression
函数中,genexpression
函数中的代码如下:
function genexpression(node, context) { const { content, isstatic } = node; context.push( isstatic ? json.stringify(content) : content, -3 /* unknown */, node ); }
由于当前的msg
变量是一个ref
响应式变量,所以isstatic
为false
。所以会执行push
方法,将$setup.msg
插入到render函数中。
执行完push
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数基本已经生成了,剩下的就是调用push
方法生成各个函数的右括号")"和右花括号"}"。将断点逐层走出,直到generate
函数中。代码如下:
function generate(ast) { // ...省略 gennode(ast.codegennode, context); deindent(); push(`}`); return { ast, code: context.code, }; }
执行完最后一个 push
方法后,在debug终端看看此时的render函数字符串是什么样的,如下图:
从上图中可以看到此时的render函数终于生成啦!
总结
这是我画的我们这个场景中generate
生成render函数的流程图:
- 执行
genmodulepreamble
函数生成:import { xxx } from "vue";
- 简单字符串拼接生成render函数中的函数名称和参数,也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
- 以根节点的
codegennode
属性为参数调用gennode
函数生成render函数中return的内容。- 此时传入的是虚拟节点,执行
genvnodecall
函数生成return _openblock(), _createelementblock(
和调用gennodelist
函数,生成createelementblock
函数的参数。 - 处理p标签的
tag
标签名和props
,生成createelementblock
函数的第一个和第二个参数。此时render函数return的内容为:return _openblock(), _createelementblock("p", null
- 处理p标签的children也就是
{{msg}}
节点,再次调用gennode
函数。此时node节点类型为双大括号节点,调用geninterpolation
函数。 - 在
geninterpolation
函数中会先调用push
方法,此时的render函数return的内容为:return _openblock(), _createelementblock("p", null, _todisplaystring(
。然后以node.content
为参数再次调用gennode
函数。 node.content
为$setup.msg
,是一个简单表达式节点,所以在gennode
函数中会调用genexpression
函数。执行完genexpression
函数后,此时的render函数return的内容为:return _openblock(), _createelementblock("p", null, _todisplaystring($setup.msg
- 调用push方法生成各个函数的右括号")"和右花括号"}",生成最终的render函数
- 此时传入的是虚拟节点,执行
到此这篇关于原来 vue 3 的 generate 是这样生成 render 函数的的文章就介绍到这了,更多相关原来vue3中template使用ref无需.value是因为这个内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论