0%

Vue3编译优化

编译优化指的是编译器将模板编译为渲染函数的过程中,尽可能多的提取关键信息,并以此指导生成最优代码的过程,优化的方向主要是区分动态内容和静态内容,并针对不同的内容采用不同的优化策略

动态节点收集与补丁标志:

Vue2中:渲染器在运行时得不到足够的信息,传统diff算法无法利用编译时提取得到的关键信息,这导致渲染器在运行时不可能去做相关优化

Vue3:会将编译时得到的关键信息附着在它生成的虚拟DOM上,这些信息会通过虚拟DOM传递给渲染器,最终渲染器会因为这些关键信息执行“快捷路径”,提升性能

传统虚拟DOM:

1
2
3
4
5
6
7
const vnode={
tag:'div'.
children:[
{tag:'div',children:'foo'},
{tag:'p',children:ctx.bar}
]
}

加了patchFlag:

1
2
3
4
5
6
7
const vnode={
tag:'div'.
children:[
{tag:'div',children:'foo'},
{tag:'p',children:ctx.bar,patchFlag:1}//动态节点
]
}

patchFlag属性就是补丁标志。理解为一系列数字标记,不同数字值的不同赋予它不同意义:

1:代表节点有动态的textContent

2:代表元素有动态class绑定

3:代表元素有动态style绑定

我们可以在虚拟结点的创建阶段,将它的动态子节点提取出来,并将其存储到该虚拟结点的dynamicChildren中,与普通虚拟节点比较,它多出了一个dynamicChildren,把含有该属性的虚拟节点称为块,即Block,一个Block还需要可以收集它的所有动态子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const PatchFlags={
TEXT:1,
CLASS:2,
STYLE:3
}
const vnode={
tag:'div'.
children:[
{tag:'div',children:'foo'},
{tag:'p',children:ctx.bar,patchFlag:1}//动态节点
],
//将children中的动态节点提取到dynamicChildren数组
dynamicChildren:[
{tag:'p',children:ctx.bar,patchFlag:PatchFlags.TEXT}//动态节点
]
}

收集所有动态子节点:

在渲染函数内,对createVNode的函数的调用是层层嵌套,并且函数的执行顺序是内层先执行,外层后执行

1
2
3
4
5
6
7
render(){ 
return createVNode('div',{},[
createVNode('div',{},[
...
])
])
}

当外层createVNode函数执行时,内层的createVNode已经执行完毕,因此,为了让外层的Block结点能够收集到内层结点,就需要一个栈结构的数据来临时存储内层的动态节点,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//动态节点栈:临时存储内层的动态节点
const dynamicChildrenStack = []
//当前动态节点的集合
let currentDynamicChildren = null
//openBlock用来创建一个新的动态节点集合,将currentDynamicChildren初始化为空数组
function openBlock() {
dynamicChildrenStack.push((currentDynamicChildren=[]))
}
//closeBlock用来将通过openBlock创建的动态节点集合从栈中弹出
function closeBlock() {
currentDynamicChildren = dynamicChildrenStack.pop()
}
//在createVNode函数内部,检测节点是否存在补丁标志,如果存在,则说明该节点是动态节点,将其添加到当前动态节点集合currentDynamicChildren中
function createVNode(tag,props,children,flags) {
const key = props && props.key
props && delete props.key
const vnode = {
tag,
props,
children,
key,
patchFlags: flags
}
if(typeof flags !== 'undefined' && currentDynamicChildren) {
//动态节点,将其添加到当前动态节点集合中
currentDynamicChildren.push(vnode)
}
}
function render() {
//...
//使用createBlock代替createNode来创建Block
//每次调用createBlcok之前先调用openBlock
return (openBlock(),createBlock('div',null,[
createVNode('p',{class: 'foo'},null,1),
createVNode('p',{class:'bar'},null)
]))
}
function createBlock(tag,props,children) {
//block本质是一个vnode
const block = createVNode(tag,props,children)
//内层的createNode函数已经执行完毕,这里的currentDynamicChildren存储的就是属于当前Block的所有动态子节点
block.dynamicChldren = currentDynamicChildren
//关闭block
closeBlock()
return block
}

利用逗号运算符保证渲染函数的返回值仍然是VNode对象,任何作为Block的节点都应该使用createBlock函数完成虚拟节点创建,由于createVNode函数和createBlock函数时由内向外,因此,当createBlock执行时,内层所有createVNode函数已经执行完毕,currentDynamicChildren存储的就是当前Block的所有动态子节点,将currentDynamicChildren赋值给dynamicChldren,完成了动态子节点的收集

渲染器运行时支持:

优先检测虚拟DOM是否存在动态节点集合,即dynamicChildren数组,如果存在,直接调用patchBlockChildren函数完成更新,这样,渲染器只会更新动态节点,而跳过所有静态结点,进行针对性的靶向更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function patchElement(n1,n2){
const el=n1.el=n2.el
const oldProps = n1.props
const newProps = n2.props
if(n2.patchFlags) {
///靶向更新
if(n2.patchFlags===1){
//只更新Text
}else if(n2.patchFlags===2) {
//更新class
}else if(n2.patchFlags===3){
//...
}else{
//全量更新
for(const key in newProps) {
if(newProps[key]!==oldProps[key]) {
patchProps(el,key,oldProps[key],newProps[key])
}
}
for(const key in oldProps) {
if(!(key in newProps)){
patchProps(el,key,oldProps[key],null)//卸载
}
}
}
}
//调用patchChildren处理children
patchChilren(n1,n2,el)
}

由于Block会收集所有动态子节点,所以对动态子节点的操作时忽略DOM层级结构的,会带来额外的问题,即v-if,v-for等结构化指令会影响DOM层级结构,使之不稳定,会间接导致Block树的对比算法失效,解决办法就是让带有v-if和v-for等指令的结点也作为Block角色,

v-if指令的结点:

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<section v-if="foo">
<p>
{{a}}
</p>
</section>
<div v-else>
<p>
{{a}}
</p>
</div>
</div>

当foo为true或false,block收集到的动态节点均是:

1
2
3
4
5
6
7
const block = {
tag:'div',
dynamicChildren: [
{tag: 'p',children: ctx.a,patchFlags:1}
]
//...
}

在Diff中就不会做更新,然而更新前后标签不一样,不做更新会产生bug,结构化指令导致更新前后模板的结构发生变化,即模板结构不稳定,因此需要让v-if/v-else等结构化指令的结点也作为Block角色

即上面这段模板会构成一颗Block树:

1
2
3
Block(div)
-Block(Section v-if)
-Block(div v-else)

父级Block除了会收集动态子节点外,也会收集子Block,因此两个子Block将作为父Block的动态节点被收集到父级Block的dynamicChildren数组中

1
2
3
4
5
6
7
const block ={
tag: 'div',
dynamicChildren:[
//Block(section v-if)或者Block(div v-else),key值根据不同Block发生变化
{tag:'section',{key:0},dynamicChildren:[...]}
]
}

v-for指令的结点:

下面的模板:

1
2
3
4
5
6
7
<div>
<p v-for="item in list">
{{item}}
</p>
<i>{{foo}}</i>
<i>{{bar}}</i>
</div>

只有最外层的div标签作为Block:

更新前:

1
2
3
4
5
6
7
8
9
10
const prevBlock = {
tag:'div',
dynamicChildren:[
{tag:'p',children:1,1},
{tag:'p',children:2,1},
{tag:'i',children:ctx.foo,1},
{tag:'i',children:ctx.bar,1}

]
}

更新后:

1
2
3
4
5
6
7
8
9
const prevBlock = {
tag:'div',
dynamicChildren:[
{tag:'p',children:1,1},
{tag:'i',children:ctx.foo,1},
{tag:'i',children:ctx.bar,1}

]
}

更新前后的block动态节点个数不一致,为了让DOM树有稳定的结构,让带有v-for指令的标签也作为Block角色,使用类型为Fragment的结点来表达v-for指令的渲染结果,并作为Block角色

1
2
3
4
5
6
7
8
9
const block = {
tag:'div',
dynamicChildren: [
//这是一个Block,它有dynamicChildren
{tag:Fragment,dynamicChildren:[/*v-for结点*/]}
{tag:'i',children:ctx.foo,1},
{tag:'i',children:ctx.bar,1}
]
}

然而Fragment本身收集的动态节点依然会结构不稳定,就是更新前后一个block的dynamicChildren数组中收集的动态节点数量或顺序不一致,导致我们无法直接进行靶向更新,只能用传统diff算法

静态提升:

1
2
3
4
5
6
7
const hoist1=createVNode('p',null,'static text'),
function render(){
return (openBlock(),createBlock('div',null,[
hoist1,
creatVNode('p',nulll,ctx.title,1)
]))
}

在渲染函数内只会持有对静态结点的引用,当响应式数据变化,是的渲染函数重新执行时,并不会重新创建静态的虚拟结点,避免了额外的性开销

预字符串化:

静态提升的虚拟节点或虚拟节点树本身是静态的,可以将其预字符串化:

比如:

1
2
3
4
5
6
<div>
<p></p>
<p></p>
...
<p></p>
</div>
1
2
3
4
5
6
7
const hoistStatic=createStaticVNode('<p></p><p></p>...')

render() {
return(openBlock(),createBlock('div',null,[
hoistStatic
]))
}

好处:

  • 大块的静态内容可以通过innerHTML进行设置,性能上有优势
  • 减少创建虚拟节点产生的性能开销和内存占用

总结

除了Block和补丁标志,Vue3还在编译性能上做了其他优化:

  • 静态提升:把纯静态的结点提升到渲染函数外,渲染函数内只会持有对静态结点的引用,当响应式数据变化,使得渲染函数重新执行时,并不会重新创建静态的虚拟结点,避免了额外的性开销
  • 预字符串化:在静态提升基础上,对静态结点进行字符串化,这样能减少创建虚拟节点产生的性能开销和内存占用,大块的静态内容可以通过innerHTML进行设置
  • 缓存内联事件处理函数:避免造成不必要的组件更新
  • v-once指令:代码中存在setBlockTracking(-1)函数调用,用来暂停动态结点的收集,也就是说使用v-once包裹的动态节点不会被父级Block收集,被v-once包裹的动态节点在组件更新时,不会参与DIff操作,缓存全部或者部分虚拟节点,避免组件更新时重新创建虚拟DOM带来的性能开销,也可以避免无用的Diff操作