解析Vue3patch外围算法patchKeyedChildren
- locate:
runtime-core > renderer > baseCreateRenderer > patchKeyedChildren
- patchKeyedChildren是patch算法中较为简单的一段,更是vue技术栈面试的高频点,最后我是在vue3.0 beta版本时大抵看了源码,起初在《vue设计与剖析》中看了霍春阳大佬的解析,本篇就简要剖析一下做个记录
- 首先patchKeyedChildren是在子列表比照并且有key的状况会进入,并且逻辑大抵分为5步,这个看源码官网正文就能够看出
第一步,从前向后遍历
这一步是从节点组头部向尾部遍历,如果遍历过程中遇到类似节点,就进行patch比照,否则就退出遍历,并记录以后遍历的最新下标
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++}
第二步,从后向前遍历
从后向前遍历,如果遇到第一步记录的下标就进行,而后遍历过程中,如果遇到类似节点也是间接进行patch比照,如果不雷同就是间接退出遍历,并且记录旧节点组和新节点组的尾指针
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--}
第三步,查看旧节点组
这一步就是查看旧节点组在上两步的遍历后是否遍历完,如果遍历完,那么新节点组没有遍历完的就都是新的dom,能够全副当作新增节点进行挂载解决
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,就是代表没有旧节点,间接将新节点插入 patch( null, (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) i++ } }}
第四步,查看新节点组
如果上一步查看旧节点未遍历完,那么就查看新节点组是否遍历完,如果遍历完,那么旧的节点组残余的节点阐明都是要卸载的,因为都不须要了
else if (i > e2) { // 旧子节点未被遍历完 while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ }}
第五步,未知序列
- 如果新旧节点组都未遍历完,阐明存在未知序列,可能存在位移等状况,就须要进一步解决
首先创立一个数组,用于记录新旧节点的对应关系
// toBePatched是新序列的节点数量 e2 - s2 + 1const newIndexToOldIndexMap = new Array(toBePatched)for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
而后会遍历旧节点组,这里会用两个变量记录
let moved = false
:位移标识,用于判断是否须要位移let patched = 0
:记录已执行patch的新节点数量,用于解决如果在更新时更新过的数量大于须要更新的节点数量,就卸载对应旧节点for (i = s1; i <= e1; i++) {const prevChild = c1[i]// 如果已更新数量大于新节点数量,就卸载节点if (patched >= toBePatched) {unmount(prevChild, parentComponent, parentSuspense, true)continue}let newIndex //新旧节点key雷同的新节点indexif (prevChild.key != null) {newIndex = keyToNewIndexMap.get(prevChild.key)} else {for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break }}}// 如果newIndex为空,则阐明未找到与旧节点对应的新节点,间接卸载if (newIndex === undefined) {unmount(prevChild, parentComponent, parentSuspense, true)} else {// 更新新旧节点关系表newIndexToOldIndexMap[newIndex - s2] = i + 1/** * 这里的maxNexIndexSoFar是记录每次patch最大index * 也是用于判断是否产生了位移,因为如果新节点index比最大index小,就阐明产生了位移 * 例如: * (a b) c * (a c b) * 在新旧序列比照b,c时,因为c最新的newIndex曾经小于b对应的newIndex,因而会记录须要位移 */if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex} else { moved = true}patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)patched++}}
- 遍历完旧序列后,就须要确定如何位移
首先是依据新旧节点关系表生成最长递增子序列(这个算法就不陈说了,较简单,能够看精读《DOM diff 最长回升子序列》),而后倒序遍历新子节点
// 最长递增子序列const increasingNewIndexSequence = moved? getSequence(newIndexToOldIndexMap): EMPTY_ARRj = increasingNewIndexSequence.length - 1// 从新序列尾部向前遍历,目标是为了应用上一个遍历的节点做锚点for (i = toBePatched - 1; i >= 0; i--) {const nextIndex = s2 + iconst nextChild = c2[nextIndex] as VNode// 如果是序列的最初一个节点,anchor就是父节点对应的anchor,否则就是上一个子节点const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor// 如果newIndexToOldIndexMap[i]还是0,那么阐明新节点没有对应的旧节点,阐明是新节点,间接挂载if (newIndexToOldIndexMap[i] === 0) { patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized )} else if (moved) { // 而后在moved为true的状况下,如果最长子序列为空或者最长子序列和以后拜访的新节点index不同,阐明就须要进行位移 if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j-- }}}