// 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);