首页 > 编程语言 >Vue3的diff算法

Vue3的diff算法

时间:2024-03-15 16:33:53浏览次数:31  
标签:node el let 算法 globalHtml key 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,移动到哪里去???
                }
            }
        }
    }
}

// 贪心算法 + 二分查找法 寻找最长递增子序列
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);
 
  
 
 

  

标签:node,el,let,算法,globalHtml,key,Vue3,diff,节点
From: https://www.cnblogs.com/MoisAbby/p/18075722

相关文章

  • Vue2的diff算法
    exportfunctiondiff(oldCh,newCh){letoldStartIndex=0;letnewStartIndex=0;letoldEndIndex=oldCh.length-1;letoldStartVnode=oldCh[0];letoldEndVnode=oldCh[oldEndIndex];letnewEndIndex=newCh.length-1;let......
  • 电机参数辨识算法(2)——基于高频注入的磁链辨识策略
    电机参数辨识算法(1)——基于高频注入的电感辨识策略-CSDN博客https://blog.csdn.net/m0_46903653/article/details/136722750?spm=1001.2014.3001.5501上一期已经讲过了电感辨识方法。今天这是参数辨识的第二期,今天来简单看看磁链的辨识。Tpwm=1e-4;%开关周期Tspeed=1e-......
  • 【算法】求 x 的平方根
    leetcode链接题目描述给你一个非负整数x,计算并返回x的算术平方根。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。注意:不允许使用任何内置指数函数和算符,例如pow(x,0.5)或者x**0.5。示例1:输入:x=4输出:2示例2:输入:x=8输出:2解释:8的......
  • 用A*算法设计搜索策略,补全关于下列走迷宫问题的程序
    补全下列关于走迷宫的程序:classNode():#TODO:完成结点类的定义,结点中要包含状态、父结点、算符等必要成员。根据算法需求,还可能包含该结点的路径代价、启发函数值、估计代价等信息def__init__(self,state,parent,action,stepCost,hCost):self.st......
  • 代码随想录算法训练营第四十七天| ● 198.打家劫舍 ● 213.打家劫舍II ● 337.打家
    打家劫舍 题目链接:198.打家劫舍-力扣(LeetCode)思路:每一家的最大收益来源只有两个,一个是这家不偷,那么最大收益等于从上一家出来的最大收益,另一个是偷这一个家,因此最大收益等于从上上一家出来的最大收益加这一家的收益。classSolution{public:introb(vector<int>&nu......
  • 代码随想录算法训练营第四十七天 | 337.打家劫舍III,213.打家劫舍II ,198.打家劫舍
     198.打家劫舍 已解答中等 相关标签相关企业 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一......
  • Light Random Sprays Retinex 传统的图像增强算法LRSR
    文章目录前言1、LightRandomSpraysRetinex概况2、LightRandomSpraysRetinex具体实现2.1、噪声去除2.2、亮度调整2.3、插值技术3、LightRandomSpraysRetinex源码4、LightRandomSpraysRetinex效果及结论前言  LightRandomSpraysRetinex,即“光随......
  • 基于python+django的协同过滤算法的小说推荐系统
    摘 要随着世界经济信息化、全球网络化的到来推动信息线上管理的飞速发展,为小说推荐的管理起到关件作用。若想达到安全,快捷的目的,就需要拥有信息化的组织和管理模式,建立一套合理、畅通、高效的小说推荐系统,通过此网站爬虫技术获取数据。当前的银行用户行为管理存在工作效率......
  • vue3 中ref和reactive的区别讲解
    1.定于数据角度对比:ref用来定义:基本类型数据reactive用来定义:对象、或数组类型的数据备注:ref也可以用来定义对象或数组类型数据,它内部会自动通过reactive转为代理对象2.原理角度对比:ref通过Object.defineProperty()的get与set来实现响应式的(数据劫持)reactive通过......
  • 聚类算法-K-means
    主要在K-means的理解1介绍K-means算法,以及具体的过程K-means算法是常用的聚类算法之一,属于无监督学习,主要用来将标签未知的数据划分成较少的类/簇,类内的样本差异要小,类间的样本差异要大,这可以帮助我们探索数据结构和分布。K-means的具体实现过程:(四步)初始化模型参数:聚类的簇......