Vue3源码解析--diff算法

diff算法的概念

patch概念引入

在vue 更新过程中在遍历子代vnode的过程中,会用不同的patch方法来patch新老vnode,如果找到对应的 newVnode 和 oldVnode,就可以复用利用里面的真实dom节点。
patch过程中,如果面对当前vnode存在有很多chidren的情况,那么需要分别遍历patch新的children Vnode和老的 children vnode。

存在children的的类型

diff算法存在patchChildren方法中,patchChildren根据是否存在key进行真正的diff或者直接patch。

diff算法的作用

存在这children的情况的vnode,需要通过patchChildren遍历children依次进行patch操作,如果在patch期间,再发现存在vnode情况,那么会递归的方式依次向下patch,那么找到与新的vnode对应的vnode显的如此重要。极大的有利于更新
如果不存在diff算法
cr见水印
Vue3源码解析--diff算法_第1张图片
而有了diff算法
第一次patchChidren
Vue3源码解析--diff算法_第2张图片
第二次patchChidren
Vue3源码解析--diff算法_第3张图片

第三次patchChidren

Vue3源码解析--diff算法_第4张图片
第四次patchChidren
Vue3源码解析--diff算法_第5张图片
diff算法会对比节点是不是一样的 是的话直接复用以前的dom节点即可

diff算法的过程

patchkeyChild

这里讨论是数组的情况diff算法的操作

①第一步从头开始向尾寻找 如果发现有不同的立即跳出

while (i <= e1 && i <= e2) {
            //

<><>

const n1 = c1[i] const n2 = c2[i] if (isSomeVnode(n1, n2)) { //嵌套 递归 patch(n1, n2, el) } else { break; //停止比对 } i++;//比对的位置 }

(a b) c

(a b) d e

②第二步从尾开始向前比对

经过1 2 操作就会对前面和后面相同的元素进行保留

③ 如果老节点是否全部patch,新节点没有被patch完,创建新的vnode

实例:
(a b)

(a b) c

i = 2, e1 = 1, e2 = 2
(a b)

c (a b)

i = 0, e1 = -1, e2 = 0
此时i>e1 添加新的元素

 if (i > e1) {
            //旧的少 新的多
            //添加数据 
            const nextPros = e2 + 1;// 插入的位置
            //前追加 e2+1
            const ancher = nextPros < c2.length ? c2[nextPros].el : null
            while (i <= e2) {
                patch(null, c2[i++], el, ancher)
            }
        }

Vue3源码解析--diff算法_第6张图片

④ 如果新节点全部被patch,老节点有剩余,那么卸载所有老节点

(a b) c

(a b)

i = 2, e1 = 2, e2 = 1

a (b c)

(b c)

i = 0, e1 = 0, e2 = -1
此时是i>e2情况出现 则调用umount去除

      else if (i>e2) {
            //旧的比新的多
            
            while(i<=e1){
                unmont(c1[i++])
            }
        }

Vue3源码解析--diff算法_第7张图片

  while (i <= e1 && i <= e2) {
            //

<><>

const n1 = c1[e1] const n2 = c2[e2] if (isSomeVnode(n1, n2)) { //嵌套 递归 patch(n1, n2, el) } else { break; //停止比对 } e1-- e2--; }

⑤ 乱序的情况

Vue3源码解析--diff算法_第8张图片
即是中间cdef部分
思路:以新的乱序个数创建一个映射表
用旧的乱序数据 在新的数据的表里面查找
有的话复用 没有的删除

(1)以下代码完成了以新乱序创建了一个表keyIndexMap
并存入新节点索引和对应的key

    let s1=i
    let s2=i
            //解决乱序比对 位置 新创建的元素创建出来
            const toBePatched=e2-s2+1//乱序的个数
            const newIndexTopatchMap=new Array(toBePatched).fill(0)

            //创建表
            let keyIndexMap=new Map();
            //用新的乱序创建表
            for(let i=s2;i<=e2;i++){
                const childVnode=c2[i]//获取到虚拟dom
                keyIndexMap.set(childVnode.key,i);
            }

(2)用老数据查找在新的数据中的索引
如果老数据存在则获取到在新的乱序中的索引并插入 没有的话则删除就像图中cde并没有变化 而f查询不带则删除
Vue3源码解析--diff算法_第9张图片
(3)最长子序列
1 虽然已经patch过所有的老节点。可以对于已经发生移动的节点,要怎么真正移动dom元素。
2 对于新增的节点,(图中节点I)并没有处理,应该怎么处理。

什么叫做最长稳定序列

首先通过getSequence得到一个最长稳定序列,对于index === 0 的情况也就是新增节点(图中I)需要从新mount一个新的vnode,然后对于发生移动的节点进行统一的移动操作
什么是最长稳定序列
对于以下的原始序列
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
最长递增子序列为
0, 2, 6, 9, 11, 15.
为什么要得到最长稳定序列
因为我们需要一个序列作为基础的参照序列,其他未在稳定序列的节点,进行移动。最长子序列是不需要移动的
然后再将需要移动的插入到对应的位置

const increasingNewINdexSequence=getSequence(newIndexTopatchMap)
            let j=increasingNewINdexSequence.length-1
            for(let i=toBePatched-1;i>=0;i--){
                let currentIndex=i+s2
                let child=c2[currentIndex]
                let ancher=currentIndex+1<c2.length? c2[currentIndex+1].el:null
                if(newIndexTopatchMap[i]==0){
                    patch(null,child,e1,ancher)
                }else{
                   if(i!=increasingNewINdexSequence[j]){
                    hostInsert(child,el,el,ancher)
                   }
                   j--;
                }
            }
function getSequence(arr:any){
    //递增
    let start
    let end
    let middle
    let len=arr.length;
    let p=arr.slice(0)
    //p是arr的数组
    const result=[0];//索引
    //(1)------处理连续递增----------
    for(let i=0;i<len;i++){
        const arrI=arr[i]//获取数组当前遍历的值
        if(arrI!=0){
            let resultLastIndex=result[result.length-1]
            if(arr[resultLastIndex]<arrI){
                //当前的之 比上一个大 直接push但是需要记住前一个兄弟
                p[i]=resultLastIndex
                //递增 添加
                result.push(i) //[0] [0,1] 相当于数是1 8 
                continue
            }
            //二分查找
            start=0;
            end=result.length-1;
            while(start<end){
                middle=((start+end)/2) | 0 //   取整 
                if(arr[result[middle]]<arrI){
                    //拿到中间值
                    start=middle+1//找arrI大的值
                }
                else{
                    end=middle
                }
            }
            //找到对应的位置
            if(arrI<arr[result[start]]){ //替换
                result[start]=i;
                if(start>0){
                    //标记到前面 记住前一个兄弟
                    p[i]=result[start-1];
                }
            }
            //循环获取数据
            let len1=result.length;//总长度
            let last=result[len1-1]//获取最后一个
            while(len1--){
                result[len1]=last
                last = p[last]

            }
        }
    }
    return result
}
 //用老数据进行查找
           for(let i=s1;i<e1;i++){
            const oldChildVnode=c1[i]
            let newIndex=keyIndexMap.get(oldChildVnode.key)//获取到老的在新的当中的索引
           // [c1,d,e,q] [e1,c,t,h]
            if(newIndex===undefined){
                //就的数据在新的表没有
                unmont(oldChildVnode)
            }
            else{
                newIndexTopatchMap[newIndex-s2]=i+1//新的数据在老的数据中的索引位置+1
                patch(oldChildVnode,c2[newIndex],el)
                //添加乱序数组
            }
           }

diff算法完整代码

   const patchkeyChild = (c1: any, c2: any, el: any) => {
        let i = 0;
        let e1 = c1.length - 1
        let e2 = c2.length - 1;
        //sync from star :头部比对 同一位置比对 知道找到不同的(停止)哪个数组没有了 也停止
        while (i <= e1 && i <= e2) {
            //

<><>

const n1 = c1[i] const n2 = c2[i] if (isSomeVnode(n1, n2)) { //嵌套 递归 patch(n1, n2, el) } else { break; //停止比对 } i++;//比对的位置 } //sync from end while (i <= e1 && i <= e2) { //

<><>

const n1 = c1[e1] const n2 = c2[e2] if (isSomeVnode(n1, n2)) { //嵌套 递归 patch(n1, n2, el) } else { break; //停止比对 } e1-- e2--; } console.log(e1, e2, i);//1 3 2 //<1> 旧的数据少 新的多(2)新的数据少 旧的数据多 if (i > e1) { //旧的少 新的多 //添加数据 const nextPros = e2 + 1;// 插入的位置 //前追加 e2+1 const ancher = nextPros < c2.length ? c2[nextPros].el : null while (i <= e2) { patch(null, c2[i++], el, ancher) } } else if (i>e2) { //旧的比新的多 while(i<=e1){ unmont(c1[i++]) } } else{ //乱序增加 思路 //以新的乱序个数创建一个映射表 //用旧的乱序数据 在新的数据的表里面查找 //有的话复用 没有的删除 let s1=i let s2=i //解决乱序比对 位置 新创建的元素创建出来 const toBePatched=e2-s2+1//乱序的个数 const newIndexTopatchMap=new Array(toBePatched).fill(0) //创建表 let keyIndexMap=new Map(); //用新的乱序创建表 for(let i=s2;i<=e2;i++){ const childVnode=c2[i]//获取到虚拟dom keyIndexMap.set(childVnode.key,i); } //用老数据进行查找 for(let i=s1;i<e1;i++){ const oldChildVnode=c1[i] let newIndex=keyIndexMap.get(oldChildVnode.key)//获取到老的在新的当中的索引 // [c1,d,e,q] [e1,c,t,h] if(newIndex===undefined){ //就的数据在新的表没有 unmont(oldChildVnode) } else{ newIndexTopatchMap[newIndex-s2]=i+1//新的数据在老的数据中的索引位置+1 patch(oldChildVnode,c2[newIndex],el) //添加乱序数组 } } //移动节点 添加新的 const increasingNewINdexSequence=getSequence(newIndexTopatchMap) let j=increasingNewINdexSequence.length-1 for(let i=toBePatched-1;i>=0;i--){ let currentIndex=i+s2 let child=c2[currentIndex] let ancher=currentIndex+1<c2.length? c2[currentIndex+1].el:null if(newIndexTopatchMap[i]==0){ patch(null,child,e1,ancher) }else{ if(i!=increasingNewINdexSequence[j]){ hostInsert(child,el,el,ancher) } j--; } } } }

总结
1 从头对比找到有相同的节点 patch ,发现不同,立即跳出。

2如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环。

3如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。

4 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )。

5不确定的元素( 这种情况说明没有patch完相同的vnode ) 与 3 ,4对立关系。建立乱序表 查找到需要不动的最长元素 对需要改变的元素进行其他地方的插入
参考文章添加链接描述

你可能感兴趣的:(算法,vue.js,前端)