本文基于
Vue 3.2.30
版本源码进行剖析为了减少可读性,会对源码进行删减、调整程序、扭转局部分支条件的操作,文中所有源码均可视作为伪代码
因为 ts 版本代码携带参数过多,不利于展现,大部分伪代码会取编译后的 js 代码,而不是原来的 ts 代码
文章内容
从《Vue3 源码 - 整体渲染流程》这篇文章中,咱们能够晓得,一开始会将咱们编写的 <template></template>
内容最终会被编译成为 render()
函数,render()
函数中执行 createBaseVNode(_createElementVNode)
/createVNode(_createVNode)
返回渲染后果造成 vnode
数据,本文将剖析和总结:
- 从源码的角度,剖析运行时编译
<template></template>
为render()
的流程 - 从源码的角度,进行
Vue3
相干编译优化的总结 - 从源码的角度,进行
Vue2
相干编译优化的总结
前置常识
单文件组件
Vue 的单文件组件 (即 *.vue
文件,英文 Single-File Component,简称 SFC) 是一种非凡的文件格式,使咱们可能将一个 Vue 组件的模板、逻辑与款式封装在单个文件中,如:
<script>
export default {data() {
return {greeting: 'Hello World!'}
}
}
</script>
<template>
<p class="greeting">{{greeting}}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>
Vue SFC 是一个框架指定的文件格式,因而必须交由 @vue/compiler-sfc 编译为规范的 JavaScript 和 CSS,一个编译后的 SFC 是一个规范的 JavaScript(ES) 模块,这也意味着在构建配置正确的前提下,你能够像导入其余 ES 模块一样导入 SFC:
import MyComponent from './MyComponent.vue'
export default {
components: {MyComponent}
}
在理论我的项目中,咱们个别会应用集成了 SFC 编译器的构建工具,比方 Vite 或者 Vue CLI (基于 webpack)
运行时编译模板
当以无构建步骤形式应用 Vue 时,组件模板要么是写在页面的 HTML 中,或者是内联的 JavaScript 字符串。在这些场景中,为了执行动静模板编译,Vue 须要将模板编译器运行在浏览器中。绝对的,如果咱们应用了构建步骤,因为提前编译了模板,那么就无须再在浏览器中运行了。为了减小打包出的客户端代码体积,Vue 提供了多种格局的“构建文件”以适配不同场景下的优化需要:
- 前缀为
vue.runtime.*
的文件是 只蕴含运行时的版本:不蕴含编译器,当应用这个版本时,所有的模板都必须由构建步骤事后编译 - 名称中不蕴含
.runtime
的文件则是 完全版:即蕴含了编译器,并反对在浏览器中间接编译模板。然而,体积也会因而增长大概 14kb
如上图所示,vue3
源码最初打包生成的文件中,有 vue.xxx.js
和vue.runtime.xxx.js
,比方 vue.global.js
和vue.runtime.global.js
留神:名称中不蕴含
.runtime
的文件蕴含了编译器,并反对在浏览器中间接编译模板,仅仅是反对编译模板,不是反对编译.vue
文件,.vue
文件还是须要构建工具,比方webpack
集成vue-loader
解析.vue
文件
在 vue.runtime.global.js
中,咱们能够发现这样的正文,如下所示,该版本不反对运行时编译
const compile$1 = () => {
{
warn$1(`Runtime compilation is not supported in this build of Vue.` +
(` Use "vue.global.js" instead.`) /* should not happen */);
}
};
在 vue.global.js
中,如上面代码所示,会进行 compile
办法的注册
function registerRuntimeCompiler(_compile) {
compile = _compile;
installWithProxy = i => {if (i.render._rc) {i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
}
};
}
registerRuntimeCompiler(compileToFunction);
在之前的文章 Vue3 源码 - 整体渲染流程浅析中,如下图所示,在触发渲染时,会触发 finishComponentSetup()
的执行
在 finishComponentSetup()
中,会检测是否存在 instance.render
以及 compile()
办法是否存在,如果不存在 instance.render
并且 compile()
办法存在,则会触发 compile()
办法执行编译流程
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
const template = Component.template;
if (template) {
{startMeasure(instance, `compile`);
}
const {isCustomElement, compilerOptions} = instance.appContext.config;
const {delimiters, compilerOptions: componentCompilerOptions} = Component;
const finalCompilerOptions = extend(extend({
isCustomElement,
delimiters
}, compilerOptions), componentCompilerOptions);
Component.render = compile(template, finalCompilerOptions);
{endMeasure(instance, `compile`);
}
}
}
instance.render = (Component.render || NOOP);
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (installWithProxy) {installWithProxy(instance);
}
}
}
接下来咱们将针对
compile()
办法执行编译流程进行具体的源码剖析
1. Vue3 的编译流程
不会执迷于具体的编译细节,只是对一个整体的流程做一个剖析
1.1 整体概述
从上面的代码块能够晓得,整个编译流程只有经验了三个阶段
baseParse()
:转化<template></template>
为ast
语法树transform()
:转化ast
数据generate()
:依据下面两个流程的ast
数据生成render()
函数
function compile(template, options = {}) {return baseCompile(template, extend({}, parserOptions, options, {...}));
}
function baseCompile(template, options = {}) {const ast = isString(template) ? baseParse(template, options) : template;
transform(ast, extend({}, options, {...}));
return generate(ast, extend({}, options, {...}));
}
注:形象语法树(abstract syntax tree 或者缩写为 AST),是源代码的形象语法结构的树状表现形式,比方上面代码块,它能够形容一段 HTML 的语法结构
const ast = {
type: 0,
children: [
{
type: 1,
tag: "div",
props:[{type: 3, name: "class", value: {}}],
children: []},
{
type: 1,
content: "这是一段文本"
}
],
loc: {}}
1.2 baseParse():生成 AST
function baseParse(content, options = {}) {const context = createParserContext(content, options);
const start = getCursor(context);
const root = parseChildren(context, 0 /* DATA */, []);
return createRoot(root, getSelection(context, start));
}
1.2.1 createParserContext():创立上下文 context
实质上是创立一个相似全局的上下文 context
,将一些办法和状态放入这个context
中,而后将这个 context
传入每一个构建 AST
的办法中,前面每一次子节点的 AST
构建,都会保护和更新这个 context
以及应用它所申明的一些根底办法
function createParserContext(content, rawOptions) {const options = extend({}, defaultParserOptions);
let key;
for (key in rawOptions) {options[key] = rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key];
}
return {
options,
column: 1,
line: 1,
offset: 0,
source: content
};
}
function getCursor(context) {const { column, line, offset} = context;
return {column, line, offset};
}
1.2.2 parseChildren- 依据不同状况构建 nodes 数组
parseChildren
次要是依据不同的条件,比方以 "<"
结尾、以 ">"
结尾,比方以 "<"
结尾,以 "/>"
结尾,比方以 "<"
结尾,以 "/>"
结尾,而后构建出对应的 node
数组寄存在 nodes
数组中,最初再解决空白字符
整体概述
function parseChildren(context, mode, ancestors) {const parent = last(ancestors);
const ns = parent ? parent.ns : 0 /* HTML */;
const nodes = [];
while (!isEnd(context, mode, ancestors)) {
const s = context.source;
let node = undefined;
if (mode === 0 /* DATA */ || mode === 1 /* RCDATA */) {
// 依据不同条件状况去调用不同的解析办法
node = xxxxxx
}
if (!node) {node = parseText(context, mode);
}
if (isArray(node)) {for (let i = 0; i < node.length; i++) {pushNode(nodes, node[i]);
}
}
else {pushNode(nodes, node);
}
}
// ... 省略解决 nodes 每一个元素空白字符的逻辑
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
如上面流程图所示,次要是依据不同的条件,调用不同的办法进行 node
的构建,次要分为:
- 标签以及标签对应的属性解析,比方
<div class="test1"></div>
- 插值的解析,如
{{refCount.count}}
- 正文节点的解析,如
<!-- 这是一段正文内容 -->
- 纯文本内容的解析,如
<div> 我是一个纯文本 </div>
中的我是一个纯文本
在通过不同条件的解析之后,咱们能够失去每一种类型的 AST
数据,比方上面代码块所示,最终 <template></template>
中的每一个元素都会转化为一个 AST
节点数据
AST 节点会依据
<template></template>
的父子关系进行数据的构建,比方 children 也是一个AST
数组汇合
{
type: 1,
tag,
tagType,
props,
children: []}
为了更好了解每一个条件的解决逻辑,咱们上面将对其中一种状况调用的办法
parseElement()
进行具体的剖析
parseElement 剖析
临时移除 Pre 和 VPre 相干逻辑
这个办法是为了解决<div class="test1"><p></p><span></span>
,为了解决这种标签,咱们一步一步地解析
- 解决
Start tag
:应用parseTag()
解决<div class="test1">
,包含标签名div
、属性值class="test1"
- 解决
children
:ancestors
压入以后的element
(为了后续解析时能正确建设父子关系),而后递归调用parseChildren()
解决<p></p><span></span>
,获取nodes
数组,弹出ancestors
压入的element
,将nodes
赋值给element.children
- 解决
End tag
:检测end Tag
是否跟element.tag
对应,如果能够对应,则更新context
存储的解析状态(相似指针一样,将指针后移到没有解析的局部)
function parseElement(context, ancestors) {
// Start tag.
const parent = last(ancestors); //ancestors[ancestors.length - 1];
const element = parseTag(context, 0 /* Start */, parent);
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {return element;}
// Children.
ancestors.push(element);
const mode = context.options.getTextMode(element, parent);
const children = parseChildren(context, mode, ancestors);
ancestors.pop();
element.children = children;
// End tag.
if (startsWithEndTagOpen(context.source, element.tag)) {parseTag(context, 1 /* End */, parent);
} else {// 找不到 endTag,进行谬误的 emit}
element.loc = getSelection(context, element.loc.start);
return element;
}
解决空白字符
次要应用正则表达式进行空白字符的剔除
空白字符也是一个文本内容,剔除空白字符能够缩小后续对这些没有意义内容的编译解决效率
function parseChildren(context, mode, ancestors) {
// ... 省略构建 nodes 数组的逻辑
// Whitespace handling strategy like v2
let removedWhitespace = false;
if (mode !== 2 /* RAWTEXT */ && mode !== 1 /* RCDATA */) {
const shouldCondense = context.options.whitespace !== 'preserve';
for (let i = 0; i < nodes.length; i++) {const node = nodes[i];
// 删除空白字符...
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
// 依据 html 标准删除前导换行符
const first = nodes[0];
if (first && first.type === 2 /* TEXT */) {first.content = first.content.replace(/^\r?\n/, '');
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
1.2.3 createRoot- 返回 root 对象的 AST 数据
从下面 parseChildren()
获取胜利 nodes
数组后,作为 children
参数传入到 createRoot()
,构建出一个ROOT
的AST
对象返回
function baseParse(content, options = {}) {const context = createParserContext(content, options);
const start = getCursor(context);
const root = parseChildren(context, 0 /* DATA */, []);
return createRoot(root, getSelection(context, start));
}
function createRoot(children, loc = locStub) {
return {
type: 0 /* ROOT */,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
};
}
1.3 transform()
从一开始的编译阶段,如上面的代码块所示,咱们能够晓得,当咱们执行实现 ast
的构建,咱们会执行 transform()
进行 ast
的转化工作
function baseCompile(template, options = {}) {
const ast = ...
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms
)
}));
return ...
}
function getBaseTransformPreset(prefixIdentifiers) {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...([]),
...([transformExpression]
),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
1.3.1 整体概述
在通过 baseParse()
将<template></template>
转化为根底的 AST
数据后,会进入一个 AST
语法分析阶段
在这个语法分析阶段中,咱们会为 AST
数据进行转化,为其增加编译优化相干的属性,比方动态晋升、Block
的构建等等
语法分析阶段会构建出信息更加丰盛的 AST
数据,为前面的 generate()
(render()
函数的生成)做筹备
function transform(root, options) {const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {hoistStatic(root, context);
}
if (!options.ssr) {createRootCodegen(root, context);
}
// finalize meta information
root.helpers = [...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;
}
1.3.2 createTransformContext()
建设一个 transform
的上下文,实质也是一个贯通 transform()
流程的一个全局变量,用来提供全局的辅助办法和存储对应的状态
其中
nodeTransforms
,directiveTransforms
是在baseCompile()
中通过getBaseTransformPreset()
失去的节点和指令转化辅助办法,比方transformIf
、on:transformOn
等辅助办法,对应<template></template>
的v-if
和v-on
造成的AST
根底数据的transform
的转化
function createTransformContext(...) {
const context = {
//...
hoists: [],
parent: null,
currentNode: root,
nodeTransforms,
directiveTransforms,
// methods
helper(name) {const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
return name;
},
replaceNode(node) {context.parent.children[context.childIndex] = context.currentNode = node;
}
//...
};
return context;
}
1.3.3 traverseNode()
如上面代码块所示,traverseNode()
逻辑次要分为三个局部:
- 执行
nodeTransforms
函数进行node
数据的解决,会依据目前node
的类型,比方是v-for
而触发对应的transformFor()
转化办法,执行结束后,拿到对应的onExit
办法,存入数组中 - 解决
node.children
,更新context.parent
和context.childIndex
,而后递归调用traverseNode(childNode)
- 解决一些
node
须要期待children
处理完毕后,再解决的状况,调用parentNode
的onExit
办法(onExit
办法也就是第一局部咱们缓存的数组)
function transform(root, options) {const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {hoistStatic(root, context);
}
if (!options.ssr) {createRootCodegen(root, context);
}
//...
}
function traverseNode(node, context) {
context.currentNode = node;
const {nodeTransforms} = context;
for (let i = 0; i < nodeTransforms.length; i++) {
// 执行 nodeTransforms 函数进行 node 数据的解决
const onExit = nodeTransforms[i](node, context);
// 缓存 onExit 办法
// 解决被删除或者被替换的状况
}
// 依据不同条件解决 node.children,实质是递归调用 traverseNode()
traverseChildren(node, context); //node.type=IF_BRANCH/FOR/ELEMENT/ROOT
// 解决一些 node 须要期待 children 处理完毕后,再解决的状况
// 即调用 parentNode 的 onExit 办法
context.currentNode = node;
let i = exitFns.length;
while (i--) {exitFns[i]();}
}
traverseNode 第 1 局部:nodeTransforms
nodeTransforms 到底是什么货色?又是如何找到指定的 node?
在一开始的初始化中,咱们就能够晓得,会默认初始化getBaseTransformPreset()
,如上面代码块所示,会初始化几个类型的解决办法
function baseCompile(template, options = {}) {
const ast = ...
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();}
function getBaseTransformPreset(prefixIdentifiers) {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...([]),
...([transformExpression]
),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
咱们随机取一个,比方 transformFor
这个解决办法
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {const { helper, removeHelper} = context;
return processFor(node, dir, context, forNode => {return () => {};});
});
从上面代码块中,咱们能够发现,每一个解决办法都会传入一个 name
字符串,而只有 node.prop.name
跟解决办法传入的 name
匹配上(matches(prop.name)
)才会执行 createStructuralDirectiveTransform()
的第二个参数 fn
,也就是下面代码块中的return processFor(xxx,xxx,xxx)
所返回的办法
function createStructuralDirectiveTransform(name, fn) {
// name = "for"
const matches = isString(name)
? (n) => n === name
: (n) => name.test(n);
return (node, context) => {if (node.type === 1 /* ELEMENT */) {const { props} = node;
if (node.tagType === 3 /* TEMPLATE */ && props.some(isVSlot)) {return;}
const exitFns = [];
for (let i = 0; i < props.length; i++) {const prop = props[i];
if (prop.type === 7 /* DIRECTIVE */ && matches(prop.name)) {props.splice(i, 1);
i--;
const onExit = fn(node, prop, context);
if (onExit)
exitFns.push(onExit);
}
}
return exitFns;
}
};
}
而执行 processFor()
最初一个参数 processCodegen
也是一个办法,从上面两个代码块,咱们能够发现 processFor
的执行程序为
- 进行
forNode
的数据的初始化,而后调用processCodegen()
办法触发createVNodeCall()
构建forNode.codegenNode
属性 - 应用
context.replaceNode(forNode)
替换以后的node
对应的AST
根底数据 - 最初返回
const onExit = nodeTransforms[i](node, context)
对应的onExit
办法,进行缓存
在上面 traverseNode 第 3 局部的逻辑中,会执行下面缓存的
onExit
办法,实际上调用的是processFor
的forNode
参数中所对应的返回的箭头函数,如上面所示
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {const { helper, removeHelper} = context;
// processCodegen()就是 forNode=>{}
return processFor(node, dir, context, forNode => {// 上面代码块 processFor()执行实现后,在执行这个 createVNodeCall()办法构建 forNode.codegenNode
forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
return () => {// 理论 onExit 办法执行的中央};
});
});
function processFor(node, dir, context, processCodegen) {const parseResult = parseForExpression(dir.exp, context);
const {source, value, key, index} = parseResult;
const forNode = {
type: 11 /* FOR */,
loc: dir.loc,
source,
...
parseResult,
children: isTemplateNode(node) ? node.children : [node]
};
//context.parent.children[context.childIndex] = context.currentNode = node;
context.replaceNode(forNode);
scopes.vFor++;
const onExit = processCodegen && processCodegen(forNode);
return () => {
scopes.vFor--;
if (onExit)
onExit();};
}
function createVNodeCall(...) {
//...
return {
type: 13 /* VNODE_CALL */,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent,
loc
};
}
traverseNode 第 2 局部:traverseChildren
依据不同条件解决 node.children
,当node.type
=IF_BRANCH
/FOR
/ELEMENT
/ROOT
时调用traverseChildren()
,实质上是递归调用traverseNode()
function traverseNode(node, context) {
//...
switch (node.type) {
case 3 /* COMMENT */:
if (!context.ssr) {context.helper(CREATE_COMMENT);
}
break;
case 5 /* INTERPOLATION */:
if (!context.ssr) {context.helper(TO_DISPLAY_STRING);
}
break;
case 9 /* IF */:
for (let i = 0; i < node.branches.length; i++) {traverseNode(node.branches[i], context);
}
break;
case 10 /* IF_BRANCH */:
case 11 /* FOR */:
case 1 /* ELEMENT */:
case 0 /* ROOT */:
traverseChildren(node, context);
break;
}
//...
}
function traverseChildren(parent, context) {
let i = 0;
const nodeRemoved = () => {i--;};
for (; i < parent.children.length; i++) {const child = parent.children[i];
if (isString(child))
continue;
context.parent = parent;
context.childIndex = i;
context.onNodeRemoved = nodeRemoved;
traverseNode(child, context);
}
}
traverseNode 第 3 局部:exitFns
由上面代码块能够晓得,nodeTransforms()
会返回一个 onExit()
办法,而后在解决实现 node.children
后,再进行逐渐调用
function traverseNode(node, context) {
context.currentNode = node;
// apply transform plugins
const {nodeTransforms} = context;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {const onExit = nodeTransforms[i](node, context);
if (onExit) {if (isArray(onExit)) {exitFns.push(...onExit);
}
else {exitFns.push(onExit);
}
}
}
//... 省略解决 node.children 的逻辑
// exit transforms
context.currentNode = node;
let i = exitFns.length;
while (i--) {exitFns[i]();}
}
而这种 onExit()
执行的内容,则依据不同条件而不同,正如咱们下面应用 transformFor
作为 nodeTransforms[i]
其中一个办法剖析一样,最终的 onExit()
执行的内容就是 processFor
的forNode
办法中所返回的办法所执行的内容
这里不再详细分析每一个
nodeTransforms[i]
所对应的onExit()
执行的内容以及作用,请读者自行参考其它文章
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {const { helper, removeHelper} = context;
return processFor(node, dir, context, forNode => {// 上面代码块 processFor()执行实现后,在执行这个 createVNodeCall()办法构建 forNode.codegenNode
forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
return () => {// 理论 onExit 办法执行的中央};
});
});
1.3.4 hoistStatic()
function transform(root, options) {const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {hoistStatic(root, context);
}
if (!options.ssr) {createRootCodegen(root, context);
}
// finalize meta information
root.helpers = [...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;
}
function hoistStatic(root, context) {
walk(root, context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0]));
}
getConstantType()返回一共有 4 种类型,为 0(
NOT_CONSTANT
)、1(CAN_SKIP_PATCH
)、2(CAN_HOIST
)、3(CAN_STRINGIFY
)
walk()
办法会进行所有 node
数据的遍历,而后依据条件判断是否须要动态晋升,依据上面的代码块,须要条件判断的状况能够总结为:
- 根结点不会动态晋升
-
如果
node
数据是一个ELEMENT
类型,会通过getConstantType()
获取常量类型- 如果
getConstantType()
>=2
,则进行动态晋升child.codegenNode = context.hoist(child.codegenNode)
- 如果
getConstantType()
返回0
,node
数据不能进行动态晋升 (可能它的children
存在动静扭转的数据等等),检测它的props
是否能够动态晋升
- 如果
- 如果
node
数据是一个文本类型,则能够进行动态晋升 - 如果
node
数据是v-for
类型,并且只有一个child
,不进行动态晋升,因为它必须是一个Block
- 如果
node
数据是v-if
类型,并且只有一个child
,不进行动态晋升,因为它必须是一个Block
function walk(node, context, doNotHoistNode = false) {const { children} = node;
const originalCount = children.length;
let hoistedCount = 0;
for (let i = 0; i < children.length; i++) {const child = children[i];
if (child.type === 1 /* ELEMENT */ && child.tagType === 0 /* ELEMENT */) {
// constantType>=2,能够动态晋升,执行上面语句
child.codegenNode.patchFlag = -1 /* HOISTED */ + (` /* HOISTED */`);
child.codegenNode = context.hoist(child.codegenNode);
// constantType === 0,自身无奈动态晋升,然而 props 可能能够
// ... 省略检测 node.props 是否动态晋升的代码
} else if (child.type === 12 /* TEXT_CALL */ &&
getConstantType(child.content, context) >= 2 /* CAN_HOIST */) {
// 文本内容
//... 省略动态晋升的代码
}
// 递归调用 walk,进行 child.children 的动态晋升
if (child.type === 1 /* ELEMENT */) {walk(child, context);
} else if (child.type === 11 /* FOR */) {
// 如果 for 只有一个 children,doNotHoistNode 置为 true,不进行动态晋升
walk(child, context, child.children.length === 1);
} else if (child.type === 9 /* IF */) {for (let i = 0; i < child.branches.length; i++) {
// 如果 if 只有一个 children,doNotHoistNode 置为 true,不进行动态晋升
walk(child.branches[i], context, child.branches[i].children.length === 1);
}
}
}
// 所有 node.children[i].codegenNode 如果都动态晋升,node 的 codegenNode.children 也全副进行动态晋升
if (hoistedCount &&
hoistedCount === originalCount &&
node.type === 1 /* ELEMENT */ &&
node.tagType === 0 /* ELEMENT */ &&
node.codegenNode &&
node.codegenNode.type === 13 /* VNODE_CALL */ &&
isArray(node.codegenNode.children)) {node.codegenNode.children = context.hoist(createArrayExpression(node.codegenNode.children));
}
}
而动态晋升调用的是 context.hoist
,从上面的代码块能够晓得,实质是利用createSimpleExpression()
创立了新的对象数据,而后更新目前的 node.codegenNode
数据
// child.codegenNode = context.hoist(child.codegenNode);
hoist(exp) {if (isString(exp))
exp = createSimpleExpression(exp);
context.hoists.push(exp);
const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, 2 /* CAN_HOIST */);
identifier.hoisted = exp;
return identifier;
}
function createSimpleExpression(content, isStatic = false, loc = locStub, constType = 0 /* NOT_CONSTANT */) {
return {
type: 4 /* SIMPLE_EXPRESSION */,
loc,
content,
isStatic,
constType: isStatic ? 3 /* CAN_STRINGIFY */ : constType
};
}
1.3.5 createRootCodegen()
function transform(root, options) {const context = createTransformContext(root, options);
traverseNode(root, context);
if (options.hoistStatic) {hoistStatic(root, context);
}
if (!options.ssr) {createRootCodegen(root, context);
}
//...
}
tagType
: 0(ELEMENT)、1(COMPONENT)、2(SLOT)、3(TEMPLATE)…
从上面的代码块能够晓得,次要是为了 root
创立对应的codegenNode
- 如果目前
root
对应的模板内容<template><div id="id1"></div></template>
只有一个child
为div#id1
,那么就将其转化为Block
,而后将child.codegenNode
赋值给root.codegenNode
- 如果目前
root
对应的模板内容有多个子节点(Vue3
容许一个<template></template>
有多个子节点),须要为root
创立一个fragment
类型的codegenNode
代码
function createRootCodegen(root, context) {const { helper} = context;
const {children} = root;
if (children.length === 1) {const child = children[0];
// if the single child is an element, turn it into a block.
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode;
if (codegenNode.type === 13 /* VNODE_CALL */) {makeBlock(codegenNode, context);
}
root.codegenNode = codegenNode;
}
else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
root.codegenNode = child;
}
}
else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
let patchFlag = 64 /* STABLE_FRAGMENT */;
let patchFlagText = PatchFlagNames[64 /* STABLE_FRAGMENT */];
// check if the fragment actually contains a single valid child with
// the rest being comments
if (children.filter(c => c.type !== 3 /* COMMENT */).length === 1) {
patchFlag |= 2048 /* DEV_ROOT_FRAGMENT */;
patchFlagText += `, ${PatchFlagNames[2048 /* DEV_ROOT_FRAGMENT */]}`;
}
root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, patchFlag + (` /* ${patchFlagText} */`), undefined, undefined, true, undefined, false /* isComponent */);
}
}
1.3.6 同步 context 数据赋值到 root 中
function transform(root, options) {
//....
//finalize meta information
root.helpers = [...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;
}
1.4 generate()
通过 transform()
后的 ast
具备更多的属性,能够用来进行代码的生成
function baseCompile(template, options = {}) {const ast = isString(template) ? baseParse(template, options) : template;
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(ast, extend({}, options, {...});
return generate(ast, extend({}, options, {prefixIdentifiers}));
}
实际上 generate()
也是依照从上到下的顺利,一步一步生成代码,尽管代码逻辑并不难懂,然而十分多,为了更好的记忆和了解,将联合示例代码进行分块剖析
1.4.1 示例代码
调试的 html 代码放在 github html 调试代码, 造成的 render()函数如下所示
const _Vue = Vue
const {createVNode: _createVNode, createElementVNode: _createElementVNode, createTextVNode: _createTextVNode} = _Vue
const _hoisted_1 = {id: "app-wrapper"}
const _hoisted_2 = {id: "app-content1"}
const _hoisted_3 = /*#__PURE__*/_createTextVNode("这是第一个 item")
const _hoisted_4 = /*#__PURE__*/_createElementVNode("input", { type: "button"}, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("div", null, "这是测试第二个 item", -1 /* HOISTED */)
return function render(_ctx, _cache) {with (_ctx) {const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock} = _Vue
const _component_InnerComponent = _resolveComponent("InnerComponent")
const _component_second_component = _resolveComponent("second-component")
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("div", _hoisted_2, [
_hoisted_3,
_hoisted_4,
_hoisted_5,
_createVNode(_component_InnerComponent)
]),
_createVNode(_component_second_component, {"stat-fat": {'label': '3333'},
test: "333"
}, null, 8 /* PROPS */, ["stat-fat"])
]))
}
}
1.4.2 createCodegenContext
跟下面两个流程一样,都是生成一个贯通这个流程的上下文,带有一些状态,比方 context.code
放弃着代码生成
当然还带有一些贯通这个流程的全局辅助办法,比方 context.indent()
进行缩进的递增等等
function createCodegenContext(ast, {...}) {
const context = {
...
mode,
code: ``,
column: 1,
line: 1,
offset: 0,
push(code, node) {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;
}
1.4.3 genFunctionPreamble:生成最外层依赖和动态晋升代码
1.4.4 生成渲染函数 render、with(Web 端运行时编译)、资源申明代码
如上面图所示,因为目前只有 ast.components
的属性,因而资源申明只会创立 Component
相干的申明代码,而不会创立 ast.directives
/ast.temps
等相干代码
1.4.5 依据 ast.codegenNode 进行 genNode()代码生成
在下面的剖析中,咱们目前曾经生成到 return
的地位了,上面会触发 genNode()
办法进行 return
前面语句的生成
function generate(ast, options = {}) {const context = createCodegenContext(ast, options);
genFunctionPreamble(ast, preambleContext);
//... 省略其它生成代码
if (ast.codegenNode) {genNode(ast.codegenNode, context);
} else {push(`null`);
}
if (useWithBlock) {deindent();
push(`}`);
}
deindent();
push(`}`);
return {
ast,
code: context.code,
preamble: ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? context.map.toJSON() : undefined};
}
从上面代码块能够晓得,依据 ast.codegenNode
进行 genNode()
代码生成实质也是依据不同的 node.type
,而后调用不同的生成代码函数,咱们示例中触发的是node.type=13
的genVNodeCall()
办法
function genNode(node, context) {if (isString(node)) {context.push(node);
return;
}
if (isSymbol(node)) {context.push(context.helper(node));
return;
}
switch (node.type) {
// ... 省略十分十分多的条件判断
case 2 /* TEXT */:
genText(node, context);
break;
case 4 /* SIMPLE_EXPRESSION */:
genExpression(node, context);
break;
case 13 /* VNODE_CALL */:
genVNodeCall(node, context);
break;
}
}
}
举例剖析 genVNodeCall()
2. Vue3 编译优化总结
2.1 Block 收集动静节点
2.1.1 原理
diff
算法是一层一层进行比拟,如果层级过深 + 能够动态数据的节点占据很多数,每一次响应式更新会造成很大的性能开销
为了尽可能进步运行时的性能,Vue3
在编译阶段,会尽可能辨别动静节点和动态内容,并且解析出动静节点的数据,绑定在 dynamicChildren
中,diff
更新时能够依据 dynamicChildren
属性,疾速找到变动更新的中央,进行疾速更新
2.1.2 示例
如上面代码块所示,咱们会将层级较深的 div#app-content2_child2_child2
的动静节点数据复制一份放在 div#app-wrapper
的dynamicChildren
属性中
具体如何应用
dynamicChildren
进行疾速更新在上面的小点会开展剖析
<div id="app-wrapper">
<div id="app-content1">
这是第 1 个 item
<input type="button"/>
<div> 这是测试第 2 个 item</div>
</div>
<div id="app-content2">
<div> 这是 child2 的 child1</div>
<div id="app-content2_child2">
<span> 这是 child2 的 child2</span>
<div id="app-content2_child2_child2">{{proxy.count}}</div>
</div>
</div>
</div>
const vnode = {
"type": "div",
"props": {"id": "app-wrapper"},
"children": [...],
"shapeFlag": 17,
"patchFlag": 0,
"dynamicProps": null,
"dynamicChildren": [
{
"type": "div",
"props": {"id": "app-content2_child2_child2"},
"shapeFlag": 9,
"patchFlag": 1,
"dynamicProps": null,
"dynamicChildren": null
}
]
}
2.1.3 怎么收集动静节点
将下面示例代码转化为 render()
的函数如下所示,最终会触发 openBlock()
、createElementBlock()
等多个办法,而在这些办法中就进行 vnode.dynamicChildren
的构建
//... 省略多个动态变量的语句
return function render(_ctx, _cache) {with (_ctx) {
const {
createElementVNode: _createElementVNode,
createTextVNode: _createTextVNode,
toDisplayString: _toDisplayString,
openBlock: _openBlock,
createElementBlock: _createElementBlock
} = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_hoisted_4,
_createElementVNode("div", _hoisted_5, [
_hoisted_6,
_createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
])
])
]))
}
}
示例代码为了尽可能简略,没有囊括
Component
的办法createBlock()
下面的代码波及到三个办法:
openBlock()
_createElementVNode()
_createElementBlock()
openBlock()剖析
如上面代码块所示,openBlock()
会创立一个新的 block
数组,即currentBlock
=[]
function openBlock(disableTracking = false) {blockStack.push((currentBlock = disableTracking ? null : []));
}
_createElementBlock()剖析
_createElementBlock()
就是createElementBlock()
,示例代码为了尽可能简略,没有囊括Component
的办法createBlock()
Component
相干的 createVNode()
,通过_createVNode()
办法的整顿,最终调用也是 createBaseVNode()
办法
因而无论是 createElementBlock()
还是createBlock()
,调用都是setupBlock(createBaseVNode())
// 一般元素 Block
function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
}
// 组件 Block
function createBlock(type, props, children, patchFlag, dynamicProps) {return setupBlock(createVNode(type, props, children, patchFlag, dynamicProps, true /* isBlock: prevent a block from tracking itself */));
}
function _createVNode() {
//...
return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true);
}
上面将开展对
createBaseVNode()
的剖析
如 createBaseVNode()
上面代码所示,有两个点须要特地留神,当 isBlockNode
=false
时,
vnode.patchFlag > 0
代表有动静绑定的数据,进行以后vnode
的收集shapeFlag & 6
为COMPONENT
类型,也就是说组件类型无论有没有动静绑定的数据,都须要标注为动静节点,进行以后vnode
的收集
注:在上下面
generate()
流程的示例代码中,有一个组件尽管没有动静节点,然而因为是COMPONENT
类型,因而还是会被收集到dynamicChildren
中
function createBaseVNode(..., isBlockNode) {const vnode = {...};
// .... 省略很多代码
// track vnode for block tree
if (isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {currentBlock.push(vnode);
}
return vnode;
}
综合剖析收集动静节点的逻辑
上面代码的具体执行程序为:
openBlock()
_createElementVNode("div", _hoisted_7)
_createElementVNode("div", _hoisted_5)
- ….
_createElementBlock()
//... 省略多个动态变量的语句
return function render(_ctx, _cache) {with (_ctx) {
const {
createElementVNode: _createElementVNode,
createTextVNode: _createTextVNode,
toDisplayString: _toDisplayString,
openBlock: _openBlock,
createElementBlock: _createElementBlock
} = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_hoisted_4,
_createElementVNode("div", _hoisted_5, [
_hoisted_6,
_createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
])
])
]))
}
}
openBlock()
: 因为没有传入disableTracking
,因而触发currentBlock=[]
function openBlock(disableTracking = false) {blockStack.push((currentBlock = disableTracking ? null : []));
}
_createElementVNode("div", _hoisted_7)
: 没有传入 isBlockNode
,isBlockNode
默认为 false
,触发currentBlock.push(vnode)
代码,收集以后的 vnode
,此时的currentBlock
对应的是最外层 div#app-wrapper
的Block
exports.createElementVNode
=createBaseVNode
,_createElementVNode
就是createBaseVNode
function createBaseVNode(..., isBlockNode) {const vnode = {...};
// .... 省略很多代码
// track vnode for block tree
if (isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {currentBlock.push(vnode);
}
return vnode;
}
function setupBlock(vnode) {
vnode.dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
// function closeBlock() {// blockStack.pop();
// currentBlock = blockStack[blockStack.length - 1] || null;
// }
closeBlock();
if (isBlockTreeEnabled > 0 && currentBlock) {currentBlock.push(vnode);
}
return vnode;
}
createElementBlock()
: createBaseVNode()
的最初一个传入参数是 true
,即isBlockNode
=true
,因而不会触发上面createBaseVNode()
中的 currentBlock.push(vnode)
代码,因而不会收集vnode
// 一般元素 Block
function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
}
function createBaseVNode(..., isBlockNode) {const vnode = {...};
// .... 省略很多代码
// track vnode for block tree
if (isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {currentBlock.push(vnode);
}
return vnode;
}
在 setupBlock()
中,咱们调用 closeBlock()
复原 currentBlock = blockStack[blockStack.length - 1]
,切换到上一个Block
(每一个Block
都须要 openBlock()
创立)
function setupBlock(vnode) {
vnode.dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
// function closeBlock() {// blockStack.pop();
// currentBlock = blockStack[blockStack.length - 1] || null;
// }
closeBlock();
if (isBlockTreeEnabled > 0 && currentBlock) {currentBlock.push(vnode);
}
return vnode;
}
小结
在上面的语句执行程序中:
openBlock()
: 创立一个currentBlock
空的数组_createElementVNode("div", _hoisted_7)
: 触发createBaseVNode()
,检测到vnode.patchFlag > 0
,将以后node
退出到currentBlock
_createElementVNode("div", _hoisted_5)
:触发createBaseVNode()
,没有通过条件无奈触发currentBlock.push(vnode)
- ….
_createElementBlock()
: 触发createBaseVNode(xxx,... true)
,因为最初一个参数为true
,因而无奈触发以后vnode
退出到currentBlock
,执行createBaseVNode()
后执行setupBlock()
,回滚currentBlock
到上一个Block
//... 省略多个动态变量的语句
return function render(_ctx, _cache) {with (_ctx) {
const {
createElementVNode: _createElementVNode,
createTextVNode: _createTextVNode,
toDisplayString: _toDisplayString,
openBlock: _openBlock,
createElementBlock: _createElementBlock
} = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_hoisted_4,
_createElementVNode("div", _hoisted_5, [
_hoisted_6,
_createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
])
])
]))
}
}
2.1.4 怎么在 diff 阶段利用 Block 数据疾速更新
如上面代码所示,咱们会间接比对新旧 vnode
的dynamicChildren
,间接取出他们的动静节点进行比拟更新操作
- 如果没有
dynamicChildren
,则还是走patchChildren()
,也就是《Vue3 源码 - 整体渲染流程》文章所剖析的那样,间接新旧vnode
一级一级地比拟 - 如果有
dynamicChildren
,则触发patchBlockChildren()
,次要就是利用n1.dynamicChildren
和n2.dynamicChildren
疾速找到那个须要更新的vnode
数据,而后执行patch(oldVNode, newVNode)
这里不再对
patchBlockChildren()
具体的逻辑开展剖析,请读者参考其它文章
const patchElement = (n1, n2) => {let { patchFlag, dynamicChildren, dirs} = n2;
if (dynamicChildren) {patchBlockChildren(n1.dynamicChildren, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds);
if (parentComponent && parentComponent.type.__hmrId) {traverseStaticChildren(n1, n2);
}
} else if (!optimized) {
// full diff
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false);
}
}
const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG, slotScopeIds) => {for (let i = 0; i < newChildren.length; i++) {const oldVNode = oldChildren[i];
const newVNode = newChildren[i];
// Determine the container (parent element) for the patch.
const container =
oldVNode.el &&
(oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode) ||
oldVNode.shapeFlag & (6 /* COMPONENT */ | 64 /* TELEPORT */))
? hostParentNode(oldVNode.el)
: fallbackContainer;
patch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, true);
}
};
2.2 动态晋升
2.2.1 原理
因为动态内容没有动静节点,是固定不变的内容,不须要参加每次的渲染更新
将动态内容进行晋升,放在 render()
函数内部执行渲染创立,每次渲染更新都复用曾经存在的固定的内容,晋升性能
2.2.2 示例
如下面 Block 收集动静节点的示例一样,因为有很多固定文本的动态内容,因而会进行多个 _hoisted_xxx
的动态内容晋升,如上面代码块所示
上面的代码块是运行时编译
vnode
造成的render()
函数,为function
模式,因而会有with
语句
const _Vue = Vue
const {createElementVNode: _createElementVNode, createTextVNode: _createTextVNode} = _Vue
const _hoisted_1 = {id: "app-wrapper"}
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", { id: "app-content1"}, [
/*#__PURE__*/
_createTextVNode("这是第 1 个 item"),
/*#__PURE__*/
_createElementVNode("input", { type: "button"}),
/*#__PURE__*/
_createElementVNode("div", null, "这是测试第 2 个 item")],
-1 /* HOISTED */
)
const _hoisted_3 = {id: "app-content2"}
const _hoisted_4 = /*#__PURE__*/_createElementVNode("div", null, "这是 child2 的 child1", -1 /* HOISTED */)
const _hoisted_5 = {id: "app-content2_child2"}
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "这是 child2 的 child2", -1 /* HOISTED */)
const _hoisted_7 = {id: "app-content2_child2_child2"}
return function render(_ctx, _cache) {with (_ctx) {
const {
createElementVNode: _createElementVNode,
createTextVNode: _createTextVNode,
toDisplayString: _toDisplayString,
openBlock: _openBlock,
createElementBlock: _createElementBlock
} = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_hoisted_4,
_createElementVNode("div", _hoisted_5, [
_hoisted_6,
_createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
])
])
]))
}
}
2.2.3 动态晋升在哪里发挥作用
如示例展现,动态内容 _hoisted_x
放在 render()
函数内部执行渲染创立,在 render()
函数中持有对动态内容的援用,当从新渲染触发时,并不会创立新的动态内容,只是间接复用放在 render()
函数内部的内容
const _Vue = Vue
const {createElementVNode: _createElementVNode, createTextVNode: _createTextVNode} = _Vue
const _hoisted_1 = {id: "app-wrapper"}
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", { id: "app-content1"}, [
/*#__PURE__*/
_createTextVNode("这是第 1 个 item"),
/*#__PURE__*/
_createElementVNode("input", { type: "button"}),
/*#__PURE__*/
_createElementVNode("div", null, "这是测试第 2 个 item")],
-1 /* HOISTED */
)
const _hoisted_3 = {id: "app-content2"}
const _hoisted_4 = /*#__PURE__*/_createElementVNode("div", null, "这是 child2 的 child1", -1 /* HOISTED */)
const _hoisted_5 = {id: "app-content2_child2"}
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "这是 child2 的 child2", -1 /* HOISTED */)
const _hoisted_7 = {id: "app-content2_child2_child2"}
return function render(_ctx, _cache) {with (_ctx) {
const {
createElementVNode: _createElementVNode,
createTextVNode: _createTextVNode,
toDisplayString: _toDisplayString,
openBlock: _openBlock,
createElementBlock: _createElementBlock
} = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_hoisted_4,
_createElementVNode("div", _hoisted_5, [
_hoisted_6,
_createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
])
])
]))
}
}
2.3 事件监听缓存
当 prefixIdentifiers
=true
和cacheHandlers
=true
时,会启动事件监听缓存,防止每次从新渲染时反复创立 onClick
办法,造成额定的性能开销
3. Vue2 编译优化总结
在 vue 2.6.14
的版本中,编译相干的步骤能够总结为
parse()
:解析<template></template>
为AST
optimize()
:优化AST
语法树-
generate()
:依据优化后的AST
语法树生成对应的render()
函数下面三个编译步骤对应上面的代码
var createCompiler = createCompilerCreator(function baseCompile(
template,
options
) {var ast = parse(template.trim(), options);
if (options.optimize !== false) {optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
本大节将集中在 optimize()
阶段进行Vue2 编译优化总结
3.1 optimize 整体概述
遍历生成的 AST
语法树,检测纯动态的子树,即永远不须要扭转的 DOM
一旦咱们检测到这些子树,咱们就能够:
- 将它们晋升为常量,这样咱们就不须要在每次从新渲染时为它们创立新节点,即在触发响应式更新时,标记为动态节点不会从新生成新的节点,而是间接复用
- 在
patching
过程齐全跳过它们,晋升性能,即在patch
阶段,不会进行比照操作,间接跳过晋升性能
function optimize (root, options) {if (!root) {return}
isStaticKey = genStaticKeysCached(options.staticKeys || '');
isPlatformReservedTag = options.isReservedTag || no;
// first pass: mark all non-static nodes.
markStatic(root);
// second pass: mark static roots.
markStaticRoots(root, false);
}
那么是如何标记为以后的节点为动态节点的呢?
依照下面代码和正文,次要进行两个步骤:
markStatic()
:标记所有非动态节点markStaticRoots()
:标记动态根节点
3.2 标记所有非动态节点 markStatic
应用 isStatic(node)
对目前 node
是否是动态节点判断,比方 expression
类型则不是动态节点,text
则是动态节点
具体能够参考上面的
isStatic()
的剖析
如果 node.type === 1
,即以后node
是component or element
,那么就会判断 node.children
,因为if
语句中的节点都不在 children
中,因而在遍历 node.children
后,还要遍历下node.ifConditions.length
遍历时,触发 node.children
和node.ifConditions.length
的 markStatic()
,而后判断它们是否是动态的节点,如果它们有一个节点不是动态的,那么以后node
就不是动态的
function markStatic (node) {node.static = isStatic(node);
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {return}
for (var i = 0, l = node.children.length; i < l; i++) {var child = node.children[i];
markStatic(child);
if (!child.static) {node.static = false;}
}
if (node.ifConditions) {for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {var block = node.ifConditions[i$1].block;
markStatic(block);
if (!block.static) {node.static = false;}
}
}
}
}
上面 isStatic()
的代码中,能够总结为:
- 如果以后
node
是expression
类型,不是动态节点 - 如果以后
node
是text
类型,是动态节点 - 如果以后
node
没有应用v-pre
指令,是动态节点 -
如果以后
node
应用v-pre
指令,它要成为动态节点就得满足- 不能应用动静绑定,即不能应用
v-xxx
、@xxx
、:
等属性,比方@click
- 不能应用
v-if
、v-for
、v-else
指令 - 不能是
slot
和component
- 以后
node.parent
不能是带有v-for
的template
标签 - 以后
node
的所有属性都必须是动态节点属性,即只能是这些type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap
属性
- 不能应用动静绑定,即不能应用
function isStatic (node) {if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
小结
以后 node
必须满足肯定条件以及以后 node.children
以及 node.ifConditions
都是动态的,以后 node
能力赋值node.static
=true
3.3 标记动态根
从上面代码能够晓得,node.staticRoot
=true
的条件为:
- 以后
node
必须满足node.static
=true
- 以后
node
必须有子元素,即node.children.length > 0
- 以后
node
的子元素不能只有一个文本节点,不然staticRoot
也是false
function markStaticRoots (node, isInFor) {if (node.type === 1) {if (node.static || node.once) {node.staticInFor = isInFor;}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true;
return
} else {node.staticRoot = false;}
if (node.children) {for (var i = 0, l = node.children.length; i < l; i++) {markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}
3.5 依据 node.staticRoot 生成代码
第一步 node.static
是为了第二步 node.staticRoot
的赋值做筹备
在生成代码 generate()
中,利用的是第二步的 node.staticRoot
属性,从而触发 genStatic()
进行动态晋升代码的生成
function generate (
ast,
options
) {var state = new CodegenState(options);
// fix #11483, Root level <script> tags should not be rendered.
var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
return {render: ("with(this){return" + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
function genElement (el, state) {if (el.parent) {el.pre = el.pre || el.parent.pre;}
if (el.staticRoot && !el.staticProcessed) {return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {return genOnce(el, state)
} else if (el.for && !el.forProcessed) {return genFor(el, state)
} else if (el.if && !el.ifProcessed) {return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {return genSlot(el, state)
} else {
// component or element
// ...
return code
}
}
在 genStatic()
中会生成 _m(xxxx)
的动态代码,而 _m()
实际上就是 renderStatic()
办法
在renderStatic()
办法中,会应用 cached[index]
进行渲染内容的缓存,下一次渲染更新时会从新执行 _m(xxxx)
的动态代码,而后从 cached
中获取之前曾经渲染的内容,不必再从新创立
function genStatic(el, state) {
el.staticProcessed = true;
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
var originalPreState = state.pre;
if (el.pre) {state.pre = el.pre;}
state.staticRenderFns.push(("with(this){return" + (genElement(el, state)) + "}"));
state.pre = originalPreState;
return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') +")")
}
function renderStatic(
index,
isInFor
) {var cached = this._staticTrees || (this._staticTrees = []);
var tree = cached[index];
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {return tree}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
);
markStatic(tree, ("__static__" + index), false);
return tree
}
3.6 patch 阶段如何跳过动态代码
在 patchVnode()
阶段,一开始会进行 oldVnode === vnode
的比拟,因为新旧 vnode
都是 renderStatic()
办法获取到的 cached
数据,因而会间接触发 return
,不再执行patchVnode()
阶段残余的 diff
流程
function patchVnode (
oldVnode,
vnode,
...
) {if (oldVnode === vnode) {return}
//....
}
4. 临时搁置的问题
后续有工夫再回来解决上面的问题
- 动态晋升的类型总结:在下面
1.3.4 hoistStatic()
的阶段剖析咱们简略地剖析了什么状况下要进行动态晋升和如何生成动态晋升代码,然而咱们并没有对具体什么类型应该进行动态晋升进行总结,次要波及到getConstantType()
的剖析
参考
- Vue2.x 利用 template 模板做了什么优化?
Vue 系列其它文章
- Vue2 源码 - 响应式原理浅析
- Vue2 源码 - 整体流程浅析
- Vue2 源码 - 双端比拟 diff 算法 patchVNode 流程浅析
- Vue3 源码 - 响应式零碎 - 依赖收集和派发更新流程浅析
- Vue3 源码 - 响应式零碎 -Object、Array 数据响应式总结
- Vue3 源码 - 响应式零碎 -Set、Map 数据响应式总结
- Vue3 源码 - 响应式零碎 -ref、shallow、readonly 相干浅析
- Vue3 源码 - 整体流程浅析
- Vue3 源码 -diff 算法 -patchKeyChildren 流程浅析
- Vue3 相干源码 -Vue Router 源码解析(一)
- Vue3 相干源码 -Vue Router 源码解析(二)
- Vue3 相干源码 -Vuex 源码解析