编译优化指的是编译器将模板编译为渲染函数的过程中,尽可能多的提取关键信息,并以此指导生成最优代码的过程,优化的方向主要是区分动态内容和静态内容,并针对不同的内容采用不同的优化策略
动态节点收集与补丁标志:
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} ], 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
function openBlock() { dynamicChildrenStack.push((currentDynamicChildren=[])) }
function closeBlock() { currentDynamicChildren = dynamicChildrenStack.pop() }
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() { return (openBlock(),createBlock('div',null,[ createVNode('p',{class: 'foo'},null,1), createVNode('p',{class:'bar'},null) ])) } function createBlock(tag,props,children) { const block = createVNode(tag,props,children) block.dynamicChldren = currentDynamicChildren 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){ }else if(n2.patchFlags===2) { }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) } } } } 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:[ {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: [ {tag:Fragment,dynamicChildren:[]} {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操作