Vue源码—diff算法

1,091 阅读3分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

为什么要分析diff算法?

  1. 了解清楚diff算法,有助于写出对页面dom更新的性能更高的代码。
  2. 锻炼自己的解决问题的思维,在工作中遇到一些问题,能给出更好的解决方案。

diff算法原则?

  1. 能不移动,尽量不移动
  2. 没得办法,只好移动
  3. 实在不行,新建或删除

1、patch来自哪?

var patch = createPatchFunction();
Vue.prototype.__patch__ =  patch

2、patch来自createPatchFunction,然后我们分析这个函数

function createPatchFunction() {  
    return function patch(oldVnode, vnode, parentElm, refElm) {      
        // 页面首次加载-->没有旧节点,直接生成新节点
        if (!oldVnode) {
            createElm(vnode, parentElm, refElm);
        } 
        else {     
            // 对比新旧节点,先简单的认为只是对比根节点的tag和key就好 
            // 实际上还对比了isComment、asyncFactory等;
            // 相同时
            if (sameVnode(oldVnode, vnode)) {                
                // 比较存在的根节点(**重点分析**)
                patchVnode(oldVnode, vnode);
            } 
            // 不同时
            // 生成新节点,同时删除旧节点
            else {    
                // 替换存在的元素
                var oldElm = oldVnode.elm;                
                var _parentElm = oldElm.parentNode    
                // 创建新节点
                createElm(vnode, _parentElm, oldElm.nextSibling);   
                // 销毁旧节点
                if (_parentElm) {
                    removeVnodes([oldVnode], 0, 0);
                }
            }
        }        
        return vnode.elm
    }
}

3、分析patchVnode函数(新旧node的根节点tag和key相同时)

function patchVnode(oldVnode, vnode) { 
    if (oldVnode === vnode) return
    var elm = vnode.elm = oldVnode.elm;    
    var oldCh = oldVnode.children;    
    var ch = vnode.children;   
    // vnode不是文本节点
    if (!vnode.text) {   
        // vnode和oldVnode都有子节点且不相同
        if (oldCh && ch) {            
            if (oldCh !== ch) {
                // (**重点分析**)
                updateChildren(elm, oldCh, ch);
            }
        }    
        // 只有vnode有子节点,oldVnode,没有
        else if (ch) {   
            // 如果旧节点是本文本节点,删除其文本内容
            if (oldVnode.text) elm.textContent = '';      
            // 遍历创建vnode的子节点
            for (var i = 0; i <= ch.length - 1; ++i) {
                createElm(
                  ch[i],elm, null
                );
            }
        } 
        // 只有oldVnode有子节点,vnode,没有
        else if (oldCh) { 
            // 遍历删除oldVnode的子节点    
            for (var i = 0; i<= oldCh.length - 1; ++i) {          
                oldCh[i].parentNode.removeChild(el);
            }
        } 
        // 两个都没有子节点,并且oldVnode是文本节点
        // 删除其中的文本内容
        else if (oldVnode.text) {
            elm.textContent = '';
        }
    } 
    // vnode是纯文本节点,且与oldVnode文本不同
    // 替换oldVnode中的内容
    else if (oldVnode.text !== vnode.text) {
        elm.textContent = vnode.text;
    }
}

4、updateChildren(新旧node都有子节点且不同时)

这部分比较复杂,先理清思路再看代码:

(1)定义8个变量分别为:

  var oldStartIdx = 0;                      // olcVnode起始的index
  var oldEndIdx = oldCh.length - 1;         // olcVnode结束的index
  var oldStartVnode = oldCh[0];             // olcVnode起始的节点
  var oldEndVnode = oldCh[oldEndIdx];       // olcVnode结束的节点
  var newStartIdx = 0;                      // vnode起始的index
  var newEndIdx = newCh.length - 1;         // vnode结束的index
  var newStartVnode = newCh[0];             // vnode起始的节点
  var newEndVnode = newCh[newEndIdx         // vnode结束的节点

(2)对比

方式:旧头 == 新头、旧尾 == 新尾、旧头 == 新尾、旧尾 == 新头、逐个对比

概述过程:

1、利用while循环做对比,如果前4种对比有对比成功,则执行相应操作并对应的去更新oldStartIdx 、oldEndIdx 、oldStartVnode 、oldEndVnode ,然后进行下一轮对比。

2、如果四个都没通过,则通过修改newStartIdx来遍历vnde与oldVnode进行单个对比,处理oldVnode。最后可能oldVnode被处理完,也可能vnode被遍历完。都会停止while,最后处理剩下的那一组的节点。

详细过程:

(1)旧头 == 新头

true=>执行下面操作,然后重新进入while循环

 patchVnode(oldStartVnode, newStartVnode);  // 递归子节点
 oldStartVnode = oldCh[++oldStartIdx];      // oldStartIdx加1
 newStartVnode = newCh[++newStartIdx];      // newStartIdx加1

false=>下一步

(2)旧尾 == 新尾

true=>执行下面操作,然后重新进入while循环

 patchVnode(oldEndVnode, newEndVnode);      // 递归子节点
 oldEndVnode = oldCh[--oldEndIdx];          // oldStartIdx减1
 newEndVnode = newCh[--newEndIdx];          // newEndIdx减1

false=>下一步

(3)旧头 == 新尾

true=>执行下面操作,然后重新进入while循环

patchVnode(oldStartVnode, newEndVnode);      // 递归子节点          
// oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
parentElm.insertBefore(
    oldStartVnode.elm, 
    oldEndVnode.elm.nextSibling
);   
oldStartVnode = oldCh[++oldStartIdx];         // oldStartIdx加1
newEndVnode = newCh[--newEndIdx];             // newEndIdx减1

false=>下一步

(4)旧尾 == 新头

true=>执行下面操作,然后重新进入while循环

patchVnode(oldEndVnode, newStartVnode);        // 递归子节点            
// oldEndVnode 放到 oldStartVnode 前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];              // oldEndIdx减1
newStartVnode = newCh[++newStartIdx];          // newStartIdx减1

false=>逐个对比

(5)逐个对比

 1. 生成map表,把就节点的key全部拷贝生成如下
// key值: 对应的vnode节点的索引index
{
  1: 0,
  2: 1,
  4: 2
}
 2. 通过map中的key判断vnode的子节点是否存在于oldVnode的子节点数组中

不存在=>创建dom,放在当前oldStartVnode 前面

存在且与vnode子节点相同=> 移动旧dom,放在当前oldStartVnode 前面

存在且与vnode子节点不同=> 创建dom,放在当前oldStartVnode 前面

while结束,处理可能剩下的节点

  1. 新子节点遍历完了newStartIdx > newEndIdx

删除oldVnode中所有身下的节点

for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
    oldCh[oldStartIdx]
    .parentNode
    .removeChild(el);
}
  1. 旧子节点遍历完了oldStartIdx > oldEndIdx

创建所有剩余的vnode中的子节点。

因为旧节点已经被处理完了。所以我们找到newEndIdx 位置。就能确实节点的倒数有几个已经被处理,

然后把剩下的vnode节点全部放在,倒数被处理过去的dom前

为什么不直接逐个对比,而是要先头尾分别对比四次?

为了处理极端情况,有些刚好是收尾变化,其他没变。如果直接逐个对比,回多走循环,性能变差

function updateChildren(parentElm, oldCh, newCh) {
    var oldStartIdx = 0;    
    var oldEndIdx = oldCh.length - 1;    
    var oldStartVnode = oldCh[0];    
    var oldEndVnode = oldCh[oldEndIdx];    
    var newStartIdx = 0;    
    var newEndIdx = newCh.length - 1;    
    var newStartVnode = newCh[0];    
    var newEndVnode = newCh[newEndIdx];    
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
    // 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
    while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) {        
        if (!oldStartVnode) {
            oldStartVnode = oldCh[++oldStartIdx];
        }     
        else if (!oldEndVnode) {
            oldEndVnode = oldCh[--oldEndIdx];
        }   
        //  旧头 和新头 比较
        else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }    
        //  旧尾 和新尾 比较
        else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }                      
        // 旧头 和 新尾 比较
        else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);            
            // oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
            parentElm.insertBefore(
                oldStartVnode.elm, 
                oldEndVnode.elm.nextSibling
            );   
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }   
        //  旧尾 和新头 比较
        else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);            
            // oldEndVnode 放到 oldStartVnode 前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }        
        // 单个新子节点 在 旧子节点数组中 查找位置
        else {    
            // oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
            if (!oldKeyToIdx) {
                oldKeyToIdx = createKeyToOldIdx(
                    oldCh, oldStartIdx, oldEndIdx
                );
            }     
            // 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
            idxInOld = oldKeyToIdx[newStartVnode.key]        
            //  新孩子中,存在一个新节点,老节点中没有,需要新建 
            if (!idxInOld) {  
                //  把  newStartVnode 插入 oldStartVnode 的前面
                createElm(
                    newStartVnode, 
                    parentElm, 
                    oldStartVnode.elm
                );
            }            
            else {                
                //  找到 oldCh 中 和 newStartVnode 一样的节点
                vnodeToMove = oldCh[idxInOld];     
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode);        
                    // 删除这个 index
                    oldCh[idxInOld] = undefined;                    
                    // 把 vnodeToMove 移动到  oldStartVnode 前面
                    parentElm.insertBefore(
                        vnodeToMove.elm, 
                        oldStartVnode.elm
                    );
                }                
                // 只能创建一个新节点插入到 parentElm 的子节点中
                else {                    
                    // same key but different element. treat as new element
                    createElm(
                        newStartVnode, 
                        parentElm, 
                        oldStartVnode.elm
                    );
                }
            }            
            // 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
            newStartVnode = newCh[++newStartIdx];
        }
    }    
    // 处理剩下的节点
    if (oldStartIdx > oldEndIdx) {  
        var newEnd = newCh[newEndIdx + 1]
        refElm = newEnd ? newEnd.elm :null;        
        for (; newStartIdx <= newEndIdx; ++newStartIdx) {
            createElm(
               newCh[newStartIdx], parentElm, refElm
            );
        }
    }    
    // 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
    else if (newStartIdx > newEndIdx) {       
        for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
            oldCh[oldStartIdx].parentNode.removeChild(el);
        }
    }
}