优化
# 1. 动态节点收集与补丁标志
# diff 算法的问题
<div id="app">
<p>{{ msg }}</p>
</div>
无论哪种 diff 算法,都会一层一层的遍历。
- 对比
div节点 - 对比
p节点 - 对比
p节点下的文本节点
vue3 中的编译器会讲编译时得到的信息放到 vnode 的 dynamicChildren 中,这样在 diff 的时候,就可以直接跳过 p 节点,从而提高性能。
# patchFlags & block
<div>
<div>foo</div>
<p>{{ msg }}</p>
</div>
传统的虚拟 dom
const vnode = {
tag: 'div',
children: [
{ tag: 'div', children: 'foo' },
{ tag: 'p', children: ctx.msg }
]
}
编译优化之后的虚拟 dom, 区分了静态内容和动态内容,patchFlag 即补丁标志; 把所有的动态子节点提取出来。
block: 带有 dynamicChildren 属性的虚拟节点。
const vnode = {
tag: 'div',
children: [
{ tag: 'div', children: 'foo' },
{ tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT }
],
// 会存储该节点下的所有动态子节点
dynamicChildren: [
{ tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT }
]
}
PatchFlags 的值 点击查看 (opens new window)
当有了 Block 时,渲染器在更新的时候,只更新虚拟节点下的 dynamicChildren 中的节点,而不会更新静态节点。
什么时候虚拟节点才会变成 Block 节点呢?
所有模板的根节点,带有 v-for v-if 等指令的节点都需要作为 Block 节点。
# 动态节点的收集
<div id="app">
<p class="msg">{{ msg }}</p>
</div>
它的渲染函数
const cmp = {
render () {
return createVnode('div', { id: 'app'}, [
createVnode('p', { class: 'msg' }, text, PatchFlags.TEXT)
])
}
}
createdVnode 函数的调用是层层嵌套的,且遵循内层先执行,外层后执行的原则。
const cmp = {
render () {
return (openBlock(), createBlock('div', { id: 'app'}, [
createVnode('p', { class: 'msg' }, text, PatchFlags.TEXT)
]))
}
}
function createVnode (type, props, children, patchFlag) {
const vnode = {
tag,
props,
children,
patchFlag,
}
if(typeof patchFlag !== 'undefined' && currentDynamicChildren){
// 是动态节点就添加
currentDynamicChildren.push(vnode)
}
return vnode
}
// 动态节点栈
const dynamicChildrenStack = []
// 当前动态节点集合
const currentDynamicChildren = null
function createBlock(type, props, children){
// 创建 block
const block = createVnode(type, props, children)
block.dynamicChildren = currentDynamicChildren
// 关闭当前动态节点集合
closeBlock()
return block
}
// 创建一个新的动态节点的集合
function openBlock(){
dynamicChildrenStack.push(currentDynamicChildren = [])
}
// 从创建的栈中取出当前动态节点集合
function closeBlock(){
currentDynamicChildren = dynamicChildrenStack.pop()
}
- 当创建
Block时,内层所有的createdVnode已经执行完了, currentDynamicChildren所存储的就是当前Block的所有动态子节点- 创建完成之后,将动态节点结合从栈中弹出
createBlock (opens new window)
# 打补丁
function patchELement (n1, n2, container, anchor) {
// 单个动态节点
if(n2.patchFlag){
if(n2.patchFlag === 1){
// text
}else if(n2.patchFlag === 2){
// classs
}
// ....
}
// 多个动态节点
if(n2.dynamicChildren){
patchBlockChild(n1, n2)
}
}
function patchBlockChild (n1, n2) {
// 只更新动态节点
for (let i = 0; i < n2.dynamicChildren.length; i++) {
patchELement(n1.dydynamicChildren[i], n2.dynamicChildren[i])
}
}
# 2. block
虽然根节点必须作为 Block 节点,但是如果只有根节点,那么他不会形成 Block 树。
dynamicChildren属性是会忽略虚拟 dom 层级的
# v-if
<div>
<p v-if="show"><p>{{ msg }}</p></p>
<div v-else><div><p>{{ msg }}</p></div><</div>
</div>
收集到的dynamicChildren是 [{tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT}]
但是把 v-if 等结构化的指令作为 Block 角色就可以了
const block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT, dynamicChildren: [] }
]
}
# v-for
不稳定结构,当 v-for 有顺序要求时,只通过传统的 diff 算法来进行更新。 如果循环的数据是个常量 就可以使用dynamicChildren
dynamicChildren 是没有层级的,无序的,无法满足有序的循环列表。
# 3. 静态提升
把静态节点提升到渲染函数之外
<div>
<p>static</p>
<p class="p" a="b">{{ title }}</p>
</div>
没有静态提升
function render(){
const staticProps = { class: 'p', a: 'b' }
return (openBlock(), createBlock('div', null, [
createVnode('p', null, 'static'),
createVnode('p', staticProps, ctx.title, PatchFlags.TEXT)
]))
}
静态提升之后,只保持对静态节点的引用,即使数据有变化,也不会对静态节点重新创建虚拟节点。
静态属性也会被提升
const staticNode = createVnode('p', null, 'static')
// 静态属性提升
const staticProps = { class: 'p', a: 'b' }
function render(){
return (openBlock(), createBlock('div', null, [
staticNode,
createVnode('p', staticProps, ctx.title, PatchFlags.TEXT)
]))
}
静态提升是以树为单位的,整个 section 都会被提升,外层 div 作为根节点,默认是 Block
<div>
<section>
<div>
<p>static</p>
</div>
</section>
</div>
# 4. 预字符串化
预字符串化是基于静态提升的一种优化策略
<div>
<p>1</p>
<p>2</p>
<p>3</p>
<!-- 20个 ... -->
<p>20</p>
</div>
当包含了大量的纯静态的标签节点是,把静态节点序列化为字符串
function render(){
const staticNode = createStaticVnode('<p></p><p></p> 20个...</p>')
return (openBlock(), createBlock('div', null, [
staticNode,
]))
}
- 大的静态内容可以通过
innerHtml进行设置 - 减少虚拟节点的创建
- 减少内存的占用
# 5. 缓存内联处理函数
渲染函数的第二个参数是个 props , 每次重新渲染都会创建一个新的 props
const comp = `<Comp @click="a+b"/>`
function render(ctx){
return h(comp, {
onClick: () => ctx.a + ctx.b
})
}
渲染函数的第二个参数是一个数组 cache,可以把内联事件添加到缓存中
const comp = `<Comp @click="a+b"/>`
function render(ctx, cache){
return h(com, {
onClick: cache[0] || (cache[0] = () => ctx.a + ctx.b)
})
}
# 6. v-once
仅渲染元素和组件一次,并跳过之后的更新, 在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。
<div>
<p v-once> {{ msg }} <div>
</div>
function render(ctx, cache){
return (openBlock(), createBlock('div', null, [
cache[1] || (
// 阻止 vnode 被 block 收集
setBlockTracking(-1),
cache[1] = createVnode('p', null, ctx.msg, PatchFlags.TEXT),
// 恢复收集
setBlockTracking(1),
cache[1]
)
]))
}
- 避免组件更新,虚拟
dom的重新创建 - 避免无用的
diff算法