虚拟DOM和diff算法(snabbdom)

91 阅读1分钟

先上流程图

image.png

五角星最小量更新代码 updateChildren.js

import createElement from "./createElement";
import patchVnode from "./patchVnode";

// 判断是否是同一个节点
function checkSameVnode(a, b) {
    return a.sel === b.sel && a.key === b.key;
}

export default function updateChildren(parentElm, oldCh, newCh) {
    console.log(oldCh,newCh)
    // 四个指针
    // 旧前
    let oldStartIdx = 0;
    // 新前
    let newStartIdx = 0;
    // 旧后
    let oldEndIdx = oldCh.length - 1;
    // 新后
    let newEndIdx = newCh.length - 1;

    // 指针指向的四个节点
    // 旧前节点
    let oldStartVnode = oldCh[0];
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx];
    // 新前节点
    let newStartVnode = newCh[0];
    // 新后节点
    let newEndVnode = newCh[newEndIdx];

    let keyMap = null;
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        console.log('♥♥♥♥♥♥♥♥♥♥')
        // 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
        if (oldStartVnode === null || oldCh[oldStartIdx] === undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (oldEndVnode === null || oldCh[oldEndIdx] === undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStartVnode === null || newCh[newStartIdx] === undefined) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newEndVnode === null || newCh[newEndIdx] === undefined) {
            newEndVnode = newCh[--newEndIdx];
        } else if(checkSameVnode(oldStartVnode,newStartVnode)) {
            //新前和旧前
            console.log(" ①1 新前与旧前 命中");
            patchVnode(oldStartVnode,newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if(checkSameVnode(oldEndVnode,newEndVnode)) {
            //新后和旧后
            console.log(" ②2 新后与旧后 命中");
            patchVnode(oldEndVnode,newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            // 新后与旧前
            console.log(" ③3 新后与旧前 命中");
            patchVnode(oldStartVnode, newEndVnode);
            // 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面
            // 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
            //这里的参数是旧前节点oldStartVnode,是因为新后与旧前是相同的,
            //但是新后是没有真实dom节点(.elm)的,而所有旧节点都有elm,因为从patch函数开始每次修改oldVnode,都会修改相应的oldVnode.elm
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前与旧后
            console.log(" ④4 新前与旧后 命中");
            patchVnode(oldEndVnode, newStartVnode);
            // 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
            // 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }else {
            // 四种都没有匹配到,都没有命中
            console.log("四种都没有命中");
            // 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了
            if (!keyMap) {
                keyMap = {};
                // 记录oldVnode中的节点出现的key
                // 从oldStartIdx开始到oldEndIdx结束,创建keyMap
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key !== undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            console.log(keyMap);
            // 寻找当前项(newStartIdx)在keyMap中映射的序号
            const idxInOld = keyMap[newStartVnode.key];
            if (idxInOld === undefined) {
                // 如果 idxInOld 是 undefined 说明是全新的项,要插入
                // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            } else {
                // 说明不是全新的项,要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示我已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动,调用insertBefore也可以实现移动。
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }

            // newStartIdx++;
            newStartVnode = newCh[++newStartIdx];
        }
    }

    //循环结束
    if(newStartIdx <= newEndIdx) {
        // 说明newVndoe还有剩余节点没有处理,所以要添加这些节点
        // 需要在patchVNode.js中,最后新增一行代码:newVNode.elm = oldVNode.elm。  这样才能正确找到标杆
        const before = newCh[newEndIdx + 1] === undefined ? null : newCh[newEndIdx + 1].elm;
        // console.log(newCh[newEndIdx + 1].elm === oldCh[0].elm) //true  父子节点的elm是同一个
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            parentElm.insertBefore(createElement(newCh[i]),before)
        }
    }else if (oldStartIdx <= oldEndIdx) {
        // 说明oldVnode还有剩余节点没有处理,所以要删除这些节点
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
}

要注意在四种命中都失败时,while循环未结束,需要继续将newStartIdx指针往下移,并且建立旧结点的keymap(简化遍历,便于寻找)
当能在旧结点中找到时,说明此节点是需要移动的,需要移动到前的前面
当并没有找到时,说明是全新的节点,需要createElement将此新节点创建成dom节点并且插入到旧前的前面

每次更新只发生在旧节点的DOM上(oldVnode.elm),也就是在已经创建好的dom树上根据虚拟节点的变化不断进行插入删除等更新

代码地址: github.com/Zoushen6/st…