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

  

posted @ 2024-03-15 16:26  -桃之夭夭  阅读(26)  评论(0编辑  收藏  举报