vue3 diff第二篇:从源码看性能优化

前提:在第一篇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

企业微信截图_16233921121799.png

vue2版template转为render函数在线查看点我

企业微信截图_16233915971830.png

从上面可以发现,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 ,此时这个条件才为真

你可能感兴趣的:(vue3 diff第二篇:从源码看性能优化)