前提:在第一篇vue3 diff第一篇:diff算法代码解析我们进行了diff核心算法解析,会引发一些思考。
太长不看版:
1. 新增在同级节点非尾部位置新增或删除,都会导致新增位置以及后面的全部节点无法复用 (并不仅仅指v-for出来没key的)
2. vue3 相对于vue2 性能优化点除了lis(最长递增子序列)实现最小化移动以外,只diff动态节点是一个很大的优化点
(flutter里也有类似优化,const声明静态节点)
2021-6-18新增
这两天研究react发现在文档中有对思考一这种现象具体的场景描述,react协调
思考一、由于是同级比较,块状节点变成vdom后也有children(不管是不是v-for循环出来的),在vue3会进入patchUnkeyedChildren,那在页面新增或删除,会导致整个页面dom都会重建??
// 这是楼层板块
测试
测试
测试
...
// 这是新闻板块
测试
测试
...
针对以上结构我们新增一个header板块
测试
测试
...
// 这是楼层板块
测试
测试
测试
...
// 这是新闻板块
测试
测试
...
以上结构,如果说在末尾,也就是新闻板块下面新增footer板块,patch没问题,一一对应然后patchchildren,里面的child还能复用
但是,如果在顶部新增header板块,这就行不通了。我们再看patch代码
patchUnkeyedChildren方法简要代码
const patchChildren: PatchChildrenFn = (n1, n2,...) => {
const { patchFlag, shapeFlag } = n2
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(){}
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
1. 遍历新旧中最短的节点,依次patch,如果不是相同节点,直接卸载
const commonLength = Math.min(c1.length, c2.length)
for (let i = 0; i < commonLength; i++) {
patch(c1[i],c2[i])
}
2. 变长了就新增,变短了就删除节点
将commonLength作为 start开始循环 卸载或者,新增节点
c1.length > c2.length? unmountChildren(c1,...,commonLength) : mountChildren(c2,...,commonLength)
}
}
很明显 patch(c1[i],c2[i]) ,新节点header和旧节点floor比较,虽然能复用,但是子节点就完全不同了。
实际场景:v-if渲染,或者拖拽,删除
结论:新增在同级节点非尾部位置新增或删除,都会导致新增位置以及后面的全部节点无法复用,vue2的双端比较大体也是如此
所以:key的重要性就不必说了
并且尽量不要跨层级的修改dom
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升
思考二、在页面上很多元素都是静态不变的,这种也会参与diff吗?
这是vue3相对vue2做的优化,使用patch flag 优化静态树,只diff会变化的数据
vue3版template 转为render函数在线查看点我,该地址在线将template转为render函数,再由下图中的_createVNode,_createBlock转为vdom
vue2版template转为render函数在线查看点我
从上面可以发现,vue3使用_createBlock创建了一个fragment包裹了动态节点,并且在末尾还根据节点动态值不同分为STABLE_FRAGMENT, TEXT。如果仅仅是动态属性,就只标记了属性PROPS。具体还有事件的缓存,可以在在线地址中点击options仔细查看区别
这里是源码createBlock部分,实际上也是调用了createVNode生成节点
export function createBlock(
type: VNodeTypes | ClassComponent,
props?: Record | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
const vnode = createVNode(
type,
props,
children,
patchFlag,
dynamicProps,
true /* isBlock: prevent a block from tracking itself */
)
// save current block children on the block vnode
vnode.dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
// close block
closeBlock()
// a block is always going to be patched, so track it as a child of its
// parent block
if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
我们再看上面提到的patchUnkeyedChildren方法,里面都用到了判断,证明只有这些标记的才会参与到diff比较,静态的不会比较
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(){}
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
patchUnkeyedChildren()
}
patchflags具体有哪些标志,点我看源码
/**
*
* Patch flags can be combined using the | bitwise operator and can be checked
* using the & operator, e.g.
*
* ```js
* const flag = TEXT | CLASS
* if (flag & TEXT) { ... }
* ```
*/
export const enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1,
BAIL = -2
}
上面Flag都是使用<<运算符得到相应的对应值,这里扩展记录一下位运算
1 << 1 1往左位移一位,在二进制就是10
同理
1 << 2 1往左位移两位,在二进制就是100
& 按位与
1 & 2
实际上应该理解为二进制来看,如果任意一个位是0 则结果就是0
1的二进制表示为 0 0 0 0 0 0 1
2的二进制表示为 0 0 0 0 0 1 0
可得结果为0 0 0 0 0 0 0 ,也就是0
| 按位或则相反,如果任意一个位是1 则结果就是1
1 | 2
可得结果为0 0 0 0 0 1 1 ,也就是3
再看源码中判断
patchFlag & PatchFlags.KEYED_FRAGMENT
KEYED_FRAGMENT = 1 << 7
也就是1 0 0 0 0 0 0 0, 使用按位与来判断,其他的flag要么比1它大,要么比他小,结果都为0
只有patchFlag 等于 PatchFlags.KEYED_FRAGMENT ,此时这个条件才为真