前言:随之vue3.0beta版本的发布,vue3.0正式版本相信不久就会与我们相遇。尤玉溪在直播中也说了vue3.0的新特性typescript强烈支持,proxy响应式原理,重新虚拟dom,优化diff算法性能提升等等。小编在这里仔细研究了vue3.0beta版本diff算法的源码,并希望把其中的细节和奥妙和大家一起分享。
首先我们来思考一些大中厂面试中,很容易问到的问题:
1 什么时候用到diff算法,diff算法作用域在哪里?
2 diff算法是怎么运作的,到底有什么作用?
3 在v-for 循环列表 key 的作用是什么
4 用索引index做key真的有用? 到底用什么做key才是最佳方案。
如果遇到这些问题,大家是怎么回答的呢?我相信当你读完这篇文章,这些问题也会迎刃而解。
一 什么时候用到了diff算法,diff算法作用域?
1.1diff算法的作用域
patch概念引入
在vue update过程中在遍历子代vnode的过程中,会用不同的patch方法来patch新老vnode,如果找到对应的 newVnode 和 oldVnode,就可以复用利用里面的真实dom节点。避免了重复创建元素带来的性能开销。毕竟浏览器创造真实的dom,操纵真实的dom,性能代价是昂贵的。
patch过程中,如果面对当前vnode存在有很多chidren的情况,那么需要分别遍历patch新的children Vnode和老的 children vnode。
存在chidren的vnode类型
首先思考一下什么类型的vnode会存在children。
①element元素类型vnode
第一中情况就是element类型vnode 会存在 children vode,此时的三个span标签就是chidren vnode情况
<div>
<span> 苹果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鸭梨🍐 </span>
</div>
在vue3.0源码中 ,patchElement用于处理element类型的vnode
②flagment碎片类型vnode
在Vue3.0中,引入了一个fragment碎片概念。
你可能会问,什么是碎片?如果你创建一个Vue组件,那么它只能有一个根节点。
<template>
<span> 苹果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鸭梨🍐 </span>
</template>
这样可能会报出警告,原因是代表任何Vue组件的Vue实例需要绑定到一个单一的DOM元素中。唯一可以创建一个具有多个DOM节点的组件的方法就是创建一个没有底层Vue实例的功能组件。
flagment出现就是用看起来像一个普通的DOM元素,但它是虚拟的,根本不会在DOM树中呈现。这样我们可以将组件功能绑定到一个单一的元素中,而不需要创建一个多余的DOM节点。
<Fragment>
<span> 苹果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鸭梨🍐 </span>
</Fragment>
在vue3.0源码中 ,processFragment用于处理Fragment类型的vnode
1.2 patchChildren
从上文中我们得知了存在children的vnode类型,那么存在children就需要patch每一个
children vnode依次向下遍历。那么就需要一个patchChildren方法,依次patch子类vnode。
patchChildren
vue3.0中 在patchChildren方法中有这么一段源码
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
/* 对于存在key的情况用于diff算法 */
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
/* 对于不存在key的情况,直接patch */
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
}
}
patchChildren根据是否存在key进行真正的diff或者直接patch。
既然diff算法存在patchChildren方法中,而patchChildren方法用在Fragment类型和element类型的vnode中,这样也就解释了diff算法的作用域是什么。
1.3 diff算法作用?
通过前言我们知道,存在这children的情况的vnode,需要通过patchChildren遍历children依次进行patch操作,如果在patch期间,再发现存在vnode情况,那么会递归的方式依次向下patch,那么找到与新的vnode对应的vnode显的如此重要。
我们用两幅图来向大家展示vnode变化。
如上两幅图表示在一次更新中新老dom树变化情况。
假设不存在diff算法,依次按照先后顺序patch会发生什么
如果不存在diff算法,而是直接patchchildren 就会出现如下图的逻辑。
第一次patchChidren
第二次patchChidren
第三次patchChidren‘
第四次patchChidren
如果没有用到diff算法,而是依次patch虚拟dom树,那么如上稍微修改dom顺序,就会在patch过程中没有一对正确的新老vnode,所以老vnode的节点没有一个可以复用,这样就需要重新创造新的节点,浪费了性能开销,这显然不是我们需要的。
那么diff算法的作用就来了。
diff作用就是在patch子vnode过程中,找到与新vnode对应的老vnode,复用真实的dom节点,避免不必要的性能开销
二 diff算法具体做了什么(重点)?
在正式讲diff算法之前,在patchChildren的过程中,存在 patchKeyedChildren
patchUnkeyedChildren
patchKeyedChildren 是正式的开启diff的流程,那么patchUnkeyedChildren的作用是什么呢? 我们来看看针对没有key的情况patchUnkeyedChildren会做什么。
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.