Vue3 Diff 算法与key
先来看看官方的解释
-
key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;
-
如果不使用Key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;
-
而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;
-
-
官方的解释对于初学者来说并不好理解,比如下面的问题:
-
什么是新旧nodes,什么是VNode?
-
没有key的时候,如何尝试修改和复用的?
-
有key的时候,如何基于key重新排列的?
-
认识VNode
-
VNode的全称是Virtual Node,也就是虚拟节点;
-
事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;
-
VNode的本质是一个JavaScript的对象;
<div class="title" style="font-size:30px;color:pink;"> 哈哈哈 </div>
const vnode = { type:"div", props:{ class:"title", style:{ "font-size":"30px", color:"pink", }, }, children:"哈哈哈" }
-
如果我们不只是一个简单的div,而是有一大堆元素,那么它们应该会形成以一个VNode Tree;
<div> <p> <i>哈哈哈</i> <i>哈哈哈</i> </p> <span>嘻嘻嘻嘻</span> <strong>呵呵呵呵</strong> </div>
-
然后我们来看一个案例:这个按钮是当我们点击按钮时会在中间插入一个f
<div id="app"> </div> <template id="my-app"> <ul> <li v-for="item in letters">{{item}}</li> </ul> <button @click="insertF">insert f</button> </template> <script src="https://unpkg.com/vue@next"></script> <script> const App = { template:"#my-app", data() { return { letters: ["a","b","c","d"] } }, methods:{ insertF() { this.letters.insert(2,0,"f"); } } } Vue.create(App).mount("#app") </script>
-
我们可以确定的是,这次跟新对于
ul
和button
是不需要进行更新,需要更新的是我们li的列表:- 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表;
- 因为对于列表中a、b、c、d它们都是没有变化的;
- 在操作真实DOM的时候,我们只需要在中间插入一个f的
li
即可
-
那么Vue中对于列表的更新究竟是如何操作的呢?
- Vue事实上会对于有key和没有key会调用两个不同的方法;
- 有key,那么就使用
patchKeyedChildren
方法; - 没有key,那么就使用
patchUnkeyedChildren
方法;
-
没有key的diff算法(patchUnkeyedChildren)
const patchUnkeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i for (i = 0; i < commonLength; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) patch( c1[i], nextChild, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } if (oldLength > newLength) { // remove old unmountChildren( c1, parentComponent, parentSuspense, true, false, commonLength ) } else { // mount new mountChildren( c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength ) } }
- 首先是拿到新 旧VNode并拿到长度较小的数组
- 然后从0位置开始依次对新,旧数组中的每一个元素进行patch比较,如果没有差异就不做更新
- 如果类型有差异直接创建新的类型,如果类型一样内容有差异就更新内容。
- 如果旧VNode的长度大于新VNode的长度,直接unmount旧数组多余的元素。
- 如果新VNode的长度大于旧VNode的长度,生成新的VNode挂载上去。
-
有key的diff算法(patchKeyedChildren)
-
第一步操作是从头开始进行遍历、比较
- a和b是一致的会继续进行比较;
- c和f因为key不一致,所以就会break跳出循环;
-
第二步操作是从尾部开始进行遍历、比较
-
第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
-
第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么移除旧节点:
-
第五步是特殊情况,中间还有很多未知的或者乱序的节点:
const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let i = 0 const l2 = c2.length let e1 = c1.length - 1 // prev ending index let e2 = l2 - 1 // next ending index // 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { break } i++ } // 2. sync from end // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { break } e1-- e2-- } // 3. common sequence + mount // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( null, (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) i++ } } } // 4. common sequence + unmount // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } } // 5. unknown sequence // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else { const s1 = i // prev starting index const s2 = i // next starting index // 5.1 build key:index map for newChildren const keyToNewIndexMap: Map<string | number | symbol, number> = new Map() for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key != null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } } // 5.2 loop through old children left to be patched and try to patch // matching nodes & remove nodes that are no longer present let j let patched = 0 const toBePatched = e2 - s2 + 1 let moved = false // used to track whether any node has moved let maxNewIndexSoFar = 0 // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no corresponding old node. // used for determining longest stable subsequence const newIndexToOldIndexMap = new Array(toBePatched) for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 for (i = s1; i <= e1; i++) { const prevChild = c1[i] if (patched >= toBePatched) { // all new children have been patched so this can only be a removal unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break } } } if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { newIndexToOldIndexMap[newIndex - s2] = i + 1 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true } patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) patched++ } } // 5.3 move and mount // generate longest stable subsequence only when nodes have moved const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR j = increasingNewIndexSequence.length - 1 // looping backwards so that we can use last patched node as anchor for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // mount new patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (moved) { // move if: // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j-- } } } } }
-
-
-
所以我们可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:
- 在没有key的时候我们效率是非常低效的;
- 在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;