Vue3的diff算法
// https://github.com/vuejs/core/tree/main/packages/runtime-core/src/renderer.ts // https://github.com/vuejs/core/tree/main//packages/runtime-test/src/nodeOps.ts export function diff(oldCh, newCh) { let oldEndIndex = oldCh.length - 1; let newEndIndex = newCh.length - 1; const parentAnchor = 'tail' let i = 0; // 头部节点对比 while( i <= newEndIndex && i <= oldEndIndex) { if (!sameVNode(oldCh[i], newCh[i])) { break; } patchVNode(oldCh[i], newCh[i]); i++; } console.log('头部节点对比', i) // 尾部节点对比 while (i <= newEndIndex && i <= oldEndIndex) { if (!sameVNode(oldCh[oldEndIndex], newCh[newEndIndex])) { break; } patchVNode(oldCh[oldEndIndex], newCh[newEndIndex]); newEndIndex--; oldEndIndex--; } console.log('尾部节点对比', newEndIndex, oldEndIndex) // 老节点遍历完,新节点有剩余 if (i > oldEndIndex && i<= newEndIndex) { const pos = newEndIndex + 1; // 参照点,在页面上已经存在了的,因为如果当前是最后一个节点,那么参照点就是父节点了 const anchor = pos < newCh.length ? newCh[pos] : parentAnchor; while ( i <= newEndIndex ) { patchVNode(null, newCh[i], anchor) i++; } } // 新节点遍历完,老节点有剩余 else if (i > newEndIndex && i <= oldEndIndex ) { while (i <= oldEndIndex) { hostRemove(oldCh[i]); i++; } } // 新老节点都有剩余 else { let newStartIndex = i; let oldStartIndex = i; let moved = false const keyToNewIndexMap = new Map(); for(let i = newStartIndex; i <= newEndIndex; i++) { const nextChild = newCh[i]; keyToNewIndexMap.set(nextChild.key, i); } const toBePatched = newEndIndex - newStartIndex + 1; // 需要处理的新节点数量 let patched = 0; // 有对应新节点的老节点的数量 const newIndexToOldIndexMap = new Array(toBePatched); for (let i = 0; i < toBePatched; i++) { newIndexToOldIndexMap[i] = 0; } let maxNewIndexSoFar = 0 // // 遍历老节点 for (let i = oldStartIndex; i <= oldEndIndex; i++) { const prevChild = oldCh[i]; if (patched >= toBePatched) { // 说明新列表里的节点对应的老节点都找完了,老列表里的都是剩余的,可以直接删除节点了。 hostRemove(prevChild); // 开始下一次循环 continue; } let newIndex; if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // 遍历所有新节点查找,此老节点在新列表里是否存在 for (let j = newStartIndex; j <= newEndIndex; j ++) { if (sameVNode(prevChild, newCh[j])) { newIndex = j; break; } } } if(newIndex === undefined) { // 此老节点在新节点列表里已经不存在了 hostRemove(prevChild) } else { // 此老节点在新列表里依然存在 newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1; // 为0代表没有对应老节点 if (newIndex > maxNewIndexSoFar) { maxNewIndexSoFar = newIndex; } else { // 说明此老节点被移动到前面了 moved = true; } // 更新老节点,这俩节点key相等,这样会把旧节点位置上的节点换成新的的!!这会导致什么??哦,已经更新过属性了,可以直接复用了 patchVNode(oldCh[i], newCh[newIndex]); // 新老都有的数量++ patched++; } } console.log('global', JSON.parse(JSON.stringify(globalHtml)), newStartIndex) // newIndexToOldIndexMap > 0 的是所有可以复用的相同节点,并且已经更新过属性了 // 这个最长递增子序列就是 可以复用且不用移动的 const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [] let j = increasingNewIndexSequence.length - 1; for (let i = toBePatched - 1; i >= 0; i--) { const nextIndex = newStartIndex + i; const nextChild = newCh[nextIndex] // nextIndex 如果是最后一个节点,就贴着最后放置;不然就贴着nextIndex+1放置 const anchor = nextIndex + 1 < newCh.length ? newCh[nextIndex + 1] : parentAnchor // newIndexToOldIndexMap if (newIndexToOldIndexMap[i] === 0) { // 新节点在老节点里不存在 patchVNode(null, nextChild, anchor) } else if (moved){ // if (j >= 0 && increasingNewIndexSequence[j] === i){ j--; } else { hostInsert(nextChild, anchor); // 移动dom,移动到anchor之前 } } } } } // 贪心算法 + 二分查找法 寻找最长递增子序列 function getSequence(arr) { const preIndex = new Array(arr.length) //存放arr的元素在最长子序列里的前一个元素值(在arr里的索引) const indexResult = [0]; // 记录最长子序列的数组,但不是具体的最长子序列 let resultLastIndex, left, right, mid; const len = arr.length for(let i = 0; i < arr.length; i++) { const arrItem = arr[i] if (arrItem !== 0) {// 说明此新节点在老节点中存在 // 最长子序列的最后一个元素(实际上是在arr里的索引) resultLastIndex = indexResult[indexResult.length - 1] // 寻找最长递增子序列长度的经典过程,只不过,比的是arr元素值,存的是索引。 if (arrItem > arr[resultLastIndex]) { preIndex[i] = resultLastIndex; indexResult.push(i); // // 跳过剩余代码,开始下一次循环 // continue; } else { // 寻找第一个不小于当前数字的LIS的元素,并更新它 left = 0; right = indexResult.length - 1; while (left < right) { mid = (right + left) >> 1 if (arr[indexResult[mid]] < arrItem) { left = mid + 1; } else { right = mid; } } if (arrItem < arr[indexResult[left]]) { if (left > 0) { // 存放当前元素i在最长子序列里的前一个元素值(在arr里的索引) preIndex[i] = indexResult[left - 1] } indexResult[left] = i; // } } } } let length = indexResult.length; let prev = indexResult[length - 1] while (length-- > 0) { indexResult[length] = prev // 后移一位 prev = preIndex[prev] // } return indexResult } function sameVNode(oldNode, newNode) { // 判断VNode是否相等,如果节点类型、组件标识、key相同,就认为是相同的 // 这里我简化了只比较key, // 如果认为是相同就调用patch进一步比较和更新,比如比较属性、事件监听器、子节点比较、组件的状态和props触发组件更新 if (oldNode.key === newNode.key) return true else { return false } } function patchVNode(oldNode, newNode, anchor) { // 进一步比较和更新,比如比较属性、事件监听器、子节点比较、组件的状态和props触发组件更新 // 还有更新或替换 dom if (oldNode === null) { insertBefore(newNode, anchor) return; } if (oldNode.key === newNode.key) { replaceChild(newNode, oldNode) } } function hostRemove(node) { // 删除node对应的dom removeChild(node); } function hostInsert(node, anchor) { insertBefore(node, anchor); } // appendChild 将一个节点添加到指定父节点的子列表末尾 // 模拟appendChild function appendChild (node) { globalHtml.push(node); } // insertBefore(node, child) 将一个节点插入到指定父节点的子列表中的参考节点之前 // 我是为了模拟JS原生方法 // 如果node已经挂载在页面上,insertBefore(node, child)的行为是移动 node 到新的位置,而不是复制它。 function insertBefore(node, anchor) { if (anchor === 'tail') { appendChild(node) } else { const oldIndex = globalHtml.findLastIndex(b => b.key === node.key); if (oldIndex > -1) { globalHtml.splice(oldIndex, 1); } const index = globalHtml.findLastIndex(b => b.key === anchor.key); globalHtml.splice(index, 0, node); } } // replaceChild 替换父节点的子节点 // 模拟原生replaceChild方法 function replaceChild(newNode, oldNode) { const index = globalHtml.findIndex(b => b.key === oldNode.key); globalHtml[index].el = newNode.el; globalHtml[index].status = '被更新了属性'; } function removeChild(node) { // 这里为了模拟所以需要findIndex,真实dom节点的删除,只需要removeChild(childNode) // 不需要index,只需要节点本身 const index = globalHtml.findIndex(b => b.key === node.key); globalHtml.splice(index, 1); } export function init(page) { globalHtml = page; } let oldCh = [ {key: 1, el: 'a', type: 'old'}, {key: 2, el: 'b', type: 'old'}, {key: 3, el: 'c', type: 'old'}, {key: 4, el: 'd', type: 'old'}, {key: 5, el: 'e', type: 'old'}, {key: 6, el: 'f', type: 'old'}, ] let newCh = [ {key: 3, el: 'c', type: 'new'}, {key: 4, el: 'd', type: 'new'}, {key: 2, el: 'b', type: 'new'}, {key: 1, el: 'a', type: 'new'}, {key: 6, el: 'f', type: 'new'}, {key: 7, el: 'g', type: 'new'}, ] let globalHtml = [...oldCh] init(globalHtml) diff(oldCh, newCh); console.log(globalHtml);