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都会重建??

// 这是楼层板块
<div class="floor">
   <p>测试</p>
   <p>测试</p>
   <span>测试</span>
   ...
</div>
// 这是新闻板块
<div  class="article">
   <h1>测试</h1>
   <span>测试</span>
   ...
</div>
针对以上结构我们新增一个header板块
<div  class="header">
    <h2>测试</h2>
     <p>测试</p>
    ...
</div>
// 这是楼层板块
<div  class="floor">
   <p>测试</p>
   <p>测试</p>
   <span>测试</span>
   ...
</div>
// 这是新闻板块
<div  class="article">
   <h1>测试</h1>
   <span>测试</span>
   ...
</div>

以上结构,如果说在末尾,也就是新闻板块下面新增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<string, any> | 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 ,此时这个条件才为真
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容