Vue3模版编译模版编译就是将模版(通常是template 标签)解析转换生成渲染函数的过程。这个渲染函数在 Vue 的
什么是模版编译?
模版编译就是将模版(通常是template
标签)解析转换生成渲染函数的过程。这个渲染函数在 Vue 的运行时环境中被执行,生成渲染所需的虚拟 DOM。
Vue 3 Template Explorer可以直观的看到模版编译生成的render
函数:
模版:
<div>
<div key="level 1">text</div>
<div key="level 2">
<div key="secondLevel">secondLevel: {{a}} </div>
</div>
</div>
生成的渲染函数:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("div", { key: "level 1" }, "text"),
_createElementVNode("div", { key: "level 2" }, [
_createElementVNode("div", { key: "secondLevel" }, "secondLevel: " + _toDisplayString(_ctx.a), 1 /* TEXT */)
])
]))
}
模版编译流程
- 解析(Parse) :解析模板字符串,转换成AST。AST 包含了节点的层级关系、属性、事件绑定等信息。
- 转换(Transform) : 将解析后的AST 转化成可以描述js代码的AST。优化的过程主要包括标记动态节点、静态节点、生成函数等属性。
- 生成渲染函数(Generate) :最后,将可以描述js代码的AST转化成
render
函数。函数可以在运行时调用,生成渲染所需的虚拟 DOM。
具体实现
模版编译的核心实现在@vue/compiler-core
中,进入node_module/@vue/compiler-core/dist/compiler-core.cjs.js
查看代码:
function baseCompile(source, options = {}) {
// ...
// 1.调用baseParse函数返回ast
const ast = shared.isString(source) ? baseParse(source, resolvedOptions) : source;
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(prefixIdentifiers);
// ...
// 2.调用transform对ast进行转换。
transform(
ast,
// 合并transform转换函数
shared.extend({}, resolvedOptions, {
nodeTransforms: [
...nodeTransforms,
...options.nodeTransforms || []
// user transforms
],
directiveTransforms: shared.extend(
{},
directiveTransforms,
options.directiveTransforms || {}
// user transforms
)
})
);
// 3.调用generate函数返回渲染函数
return generate(ast, resolvedOptions);
}
函数transform
主要做了三件重要的事情:
- 调用
baseParse
函数返回ast
。 - 接着调用
transform
对ast
进行转换。 - 最后调用
generate
函数返回结果。
Parse(AST的生成)
模板在经过 baseParse
处理后生成的 AST 节点会是这样:
{
children: [{
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
}],
codegenNode: undefined, //没有值
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
ns: 0,
props: [],
helper: [],
hoists: [],
tag: "div",
tagType: 0,
type: 1
}
children
中存放的就是最外层div
的后代。loc
则用来描述这个 AST 元素 在整个template
中的位置信息。type
则是用于描述这个元素的类型。
与vue2相比多了hoists
、codegenNode
、helper
等属性,这些属性目前都是空的状态,在Transform阶段会被赋值,具体实现会在Transform阶段分析。
Transform(AST的转换)
优化后的AST如所示:
{
cached: 0,
children: [{…}],
codegenNode: {type: 13, tag: '"div"', props: undefined, children: Array(2), patchFlag: undefined, …},
components: [],
directives: [],
helpers: {Symbol(createElementVNode), Symbol(toDisplayString), Symbol(openBlock), Symbol(createElementBlock)},
hoists: [],
imports: [],
loc: { start: {…}, end: {…}, source: "<div> <div key="level 1">text</div> <div key="level 2"> <div key="secondLevel">secondLevel: {{a}} </div> </div> </div> "},
temps: 0,
transformed: true,
type: 0
}
codegenNode
记录生成执行函数的准确信息,其中patchFlag
用来准确的标记该节点的类型、dynamicProps
用来收集动态节点;helpers
生成代码拼接字符串的时候会用到的具体函数;hoists
实现静态节点的创建;
具体实现:
Vue 3 引入了一种基于插件机制的转换架构(类似webpack plugin)。会根据 baseParse
生成的 AST 递归应用对应的 plugin
。
transform
从baseCompile
中代码给transform
传入了两个参数,一个参数是待转化的AST,另一个参数是一个option对象。transform
函数具体实现:
function transform(root, options) {
// 1.创建转换上下文对象;
const context = createTransformContext(root, options);
// 2.调用函数traverseNode对AST进行转化;
traverseNode(root, context);
if (options.hoistStatic) {
hoistStatic(root, context);
}
if (!options.ssr) {
createRootCodegen(root, context);
}
// 3.对根节点相关信息进行赋值
root.helpers = /* @__PURE__ */ new Set([...context.helpers.keys()]);
root.components = [...context.components];
root.directives = [...context.directives];
root.imports = context.imports;
root.hoists = context.hoists;
root.temps = context.temps;
root.cached = context.cached;
root.transformed = true;
{
root.filters = [...context.filters];
}
}
函数transfrom
主要做了下面3件事:
- 创建转换上下文;
- 调用函数
traverseNode
对AST进行转化; - 对根节点相关信息进行赋值;
-
- 对helpers、hoists进行赋值
traverseNode
函数traverseNode
是整个AST转化环节最核心的方法,其代码实现:
function traverseNode(node, context) {
context.currentNode = node;
// 1.从上下文中提取转换函数数组nodeTransforms
const { nodeTransforms } = context;
const exitFns = [];
// 2.对节点遍历调用所有的转换函数
for (let i2 = 0; i2 < nodeTransforms.length; i2++) {
// 3.命中类型的转换函数执行完毕后会返回后处理函数
const onExit = nodeTransforms[i2](node, context);
if (onExit) {
if (shared.isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}
// ...
}
switch (node.type) {
case 3:
// ...
break;
case 5:
// ...
break;
// 4.深度遍历子节点
case 9:
for (let i2 = 0; i2 < node.branches.length; i2++) {
traverseNode(node.branches[i2], context);
}
break;
case 10:
case 11:
case 1:
case 0:
// ...
}
context.currentNode = node;
// 5.遍历完子节点后执行后处理函数
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
函数transfrom
主要做了下面4件事:
- 从上下文中提取转换函数
nodeTransforms
; - 遍历执行所有的转换函数;
- 命中类型的转换函数执行完毕后会返回后处理函数;
- 深度遍历子节点;
- 遍历完子节点后执行后处理函数。
上文我们提到了插件机制的转换架构,其实就是nodeTransforms
包含了一系列函数。这些函数各自承担着对节点的某个特定部分的内容进行转化的功能,包含专门解析标签、解析文本节点等16个内置的转换函数。如果用户想要自定义转换,那直接添加一个处理函数(plugin
)即可。
transformElement
举个例子看一下每个节点都会命中的转换函数transformElement
:
export const transformElement: NodeTransform = (node, context) => {
// 转换函数返回一个后处理函数
return function postTransformElement() {
// ...
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
isComponent,
node.loc
)
}
}
createVNodeCall
再看一下createVNodeCall
函数:
export function createVNodeCall(
context: TransformContext | null,
// ...
): VNodeCall {
if (context) {
if (isBlock) {
context.helper(OPEN_BLOCK)
context.helper(getVNodeBlockHelper(context.inSSR, isComponent))
} else {
context.helper(getVNodeHelper(context.inSSR, isComponent))
}
if (directives) {
context.helper(WITH_DIRECTIVES)
}
}
return {
type: NodeTypes.VNODE_CALL,
tag, // tag: VNodeCall['tag'],
props, // props?: VNodeCall['props'],
children, // children?: VNodeCall['children'],
patchFlag, // patchFlag?: VNodeCall['patchFlag'],
dynamicProps, // dynamicProps?: VNodeCall['dynamicProps'],
directives, // directives?: VNodeCall['directives'],
isBlock, // isBlock: VNodeCall['isBlock'] = false,
disableTracking, // disableTracking: VNodeCall['disableTracking'] = false,
isComponent, // isComponent: VNodeCall['isComponent'] = false,
loc // loc = locStub
}
}
核心是为 AST Element 生成上文提及的 codegenNode
属性。
Generate(生成渲染函数)
具体实现
generate
看一下generate
函数的具体实现:
function generate(ast, options = {}) {
// 1.创建上下文对象
const context = createCodegenContext(ast, options);
if (options.onContextCreated) options.onContextCreated(context);
// 上下文对象包括了若干字符串拼接相关的方法,
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context;
// 提取节点上的参数
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== "module";
const genScopeId = scopeId != null && mode === "module";
const isSetupInlined = !!options.inline;
const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context;
// ...
// 将最外层的参数通过字符串拼接
if (useWithBlock) {
push(`with (_ctx) {`);
indent();
if (hasHelpers) {
push(
`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue
`,
-1 /* End */
);
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.filters && ast.filters.length) {
newline();
genAssets(ast.filters, "filter", context);
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(`
`, 0 /* Start */);
newline();
}
if (!ssr) {
push(`return `);
}
if (ast.codegenNode) {
// 2.拼接codegenNode中的属性生成该节点的渲染函数
genNode(ast.codegenNode, context);
} else {
push(`null`);
}
if (useWithBlock) {
deindent();
push(`}`);
}
deindent();
push(`}`);
// 3.返回结果对象,包括code属性,code属性值就是生成的render函数代码字符串。
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
map: context.map ? context.map.toJSON() : void 0
};
}
函数generate
主要做了下面4件事:
- 创建
context
,context
上包括了若干字符串拼接相关的方法:
-
- 代码缩进相关的
indent
、deindent
函数; - 代码拼接函数
push
;
- 代码缩进相关的
- 使用
genNode
函数根据属性codegenNode
中的属性生成该节点的渲染函数; - 返回结果对象,包括
code
属性,code
属性值就是生成的render
函数代码字符串。
genNode
在函数generate
就是根据属性codegenNode
的值生成渲染函数的过程,看一下具体实现:
function genNode(node, context) {
if (shared.isString(node)) {
context.push(node, -3 /* Unknown */);
return;
}
if (shared.isSymbol(node)) {
context.push(context.helper(node));
return;
}
switch (node.type) {
case 1:
case 9:
case 11:
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:
genNode(node.codegenNode, context);
break;
case 8:
genCompoundExpression(node, context);
break;
case 3:
genComment(node, context);
break;
case 13:
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:
genTemplateLiteral(node, context);
break;
case 23:
genIfStatement(node, context);
break;
case 24:
genAssignmentExpression(node, context);
break;
case 25:
genSequenceExpression(node, context);
break;
case 26:
genReturnStatement(node, context);
break;
case 10:
break;
default:
{
assert(false, `unhandled codegen node type: ${node.type}`);
const exhaustiveCheck = node;
return exhaustiveCheck;
}
}
}
核心根据不同的节点类型单独进行处理生成代码,因为不同类型的节点的代码结构上不相同的。
跟vue2比较起来的优化点有哪些?
Q.对于那些静态节点每次更新都需要重新编译生成渲染函数吗?
1.静态提升
什么是静态提升?
Vue 编译器自动地会提升完全静态节点的渲染函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。
哪些内容会被静态提升?
静态的props 属性
模版:
<div>
<div>text</div>
<div id="foo" class="bar" key="secondLevel">
{{ text }}
</div>
</div>
编译后:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = {
id: "foo",
class: "bar",
key: "secondLevel"
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (_cache[0] = _createElementVNode("div", null, "text", -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createTextVNode()),
_createElementVNode("div", _hoisted_1, _toDisplayString(_ctx.text), 1 /* TEXT */)
]))
}
// Check the console for the AST
完全静态的节点
模版:
<div>
<div key="firstLevel 001">firstLevel</div>
<div key="firstLevel 002">secondLevel</div>
</div>
编译后:
import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "firstLevel 001" }, "firstLevel", -1 /* HOISTED */),
_createTextVNode(),
_createElementVNode("div", { key: "firstLevel 002" }, "secondLevel", -1 /* HOISTED */)
])))
}
// Check the console for the AST
连续的静态节点
模版:
<div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div>{{ dynamic }}</div>
</div>
编译优化后:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (_cache[0] = _createStaticVNode("<div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div>", 5)),
_createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}
// Check the console for the AST
当有足够多连续的静态元素时,它们会被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。这些静态节点会直接通过 innerHTML
来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的 cloneNode()
方法来克隆新的 DOM 节点,这会非常高效。
Q.更新时我们只需要关注比较动态的内容就可以,那么如何减少不必要的比较呢?
2.Block 机制
什么是Block机制?
将模版树通过某种策略拆分为块,把每一区块(树的分支)内的动态节点都收集到一个数组中,对树的动态节点进行打平。Block
本质是一种特殊的vnode
,它与普通vnode
相比,多出了一个dynamicChildren
属性,这个属性中保存了该区块下所有的动态节点(不只是直接子节点)。
每一个块都会追踪其所有带更新类型标记的后代节点 ,举例来说:
<div> <!-- root block -->
<div>...</div> <!-- 不会追踪 -->
<div :id="id"></div> <!-- 要追踪 -->
<div> <!-- 不会追踪 -->
<div>{{ bar }}</div> <!-- 要追踪 -->
</div>
</div>
编译的结果会被打平为一个数组,仅包含所有动态的后代节点:
div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定
哪些节点会被定义为Block?
内部结构是稳定的一个部分可被称之为一个区块Block。
什么是block tree
block Tree其实就是把带有v-for
、v-if/v-else-if/v-else
这些DOM结构可能发生改变的地方作为一个block节点进行收集。
<div>
<div v-if="flag1" >foo</div>
<div v-else >foo</div>
<div v-for="(item,index) in list" :key="index">
<div v-else >{{ item }}</div>
</div>
<div>{{ dynamic }}</div>
</div>
动态节点内部也会被打平为一个数组:
div (block tree root)
- div(v-if block) 带有 flag1 绑定
- div(v-else block)
- div(v-for block)
- div 带有 {{ item }} 绑定
- div 带有 {{ item }} 绑定
- ...
- div 带有 {{ dynamic }} 绑定
当组件需要重渲染时,只需要遍历这个打平的树而非整棵树。
他山之石
转载自:https://juejin.cn/post/7416247734552887311