基于snabbdom的diff算法的学习笔记-04最小化更新updateChildren

最小化更新updateChildren

基本概念

文档中所用的虚拟节点模型

// 老节点
const oldVnode = h('ul', {}, [
   h('li', { key: 'A'}, '我是A'),
   h('li', { key: 'B'}, '我是B'),
   h('li', { key: 'C'}, '我是C'),
])
// 模型一
const newVnode = h('ul', {}, [
   h('li', { key: 'A'}, '我是AAA'),
   h('li', { key: 'B'}, '我是BBB'),
   h('li', { key: 'M'}, '我是M'),
   h('li', { key: 'N'}, '我是N'),
   h('li', { key: 'C'}, '我是CCC'),
])
// 模型二
const newVnode = h('ul', {}, [
   h('li', { key: 'B'}, '我是BBB'),
   h('li', { key: 'M'}, '我是M'),
   h('li', { key: 'N'}, '我是N'),
   h('li', { key: 'C'}, '我是CCC'),
])
// 模型三
const newVnode = h('ul', {}, [
   h('li', { key: 'B'}, '我是BBB'),
   h('li', { key: 'M'}, '我是M'),
   h('li', { key: 'N'}, '我是N'),
   h('li', { key: 'C'}, '我是CCC'),
   h('li', { key: 'A'}, '我是AAA'),
])
// 模型四
const newVnode = h('ul', {}, [
   h('li', { key: 'C'}, '我是CCC'),
   h('li', { key: 'A'}, '我是AAA'),
   h('li', { key: 'B'}, '我是BBB'),
   h('li', { key: 'M'}, '我是M'),
   h('li', { key: 'N'}, '我是N'),
])
// 模型五
const newVnode = h('ul', {}, [
   h('li', { key: 'A'}, '我是AAA'),
])
// 模型6
const newVnode = h('ul', {}, [
   h('li', { key: 'B'}, '我是BBB'),
    h('li', { key: 'M'}, '我是M'),
   h('li', { key: 'A'}, '我是AAA'),
   h('li', { key: 'C'}, '我是CCC'),
   h('li', { key: 'N'}, '我是N'),
])

四种指针

  • 新前:新前指针(newStartIdx),指向新的虚拟节点未处理的第一个节点,初始化为0
  • 新后:新后指针(newEndIdx),指向新的虚拟节点未处理的最后一个节点,初始化为 newVnode.children.length - 1
  • 旧前:旧前指针(oldStartIdx),指向旧的虚拟节点未处理的第一个节点, 初始化为0
  • 旧后:旧后指针(oldEndIdx),指向旧的虚拟节点未处理的最后一个节点,初始化为oldVnode.children.length-1
    在这里插入图片描述

四种命中

  • 新前与旧前:新前指针指向的元素与旧前指针指向同一节点(同一节点定义请看第二篇),如模型一新前和旧前指针指向的都是h('li', {key: 'A'}, ''), 当命中时更新该节点,并将新前和旧前指针下移
  • 新后与旧后:新后指针和旧后指针指向同一节点,如模型二第一次循环时新后与旧后都指向h('li', { key: 'C'}, ''),,当命中时更新该节点,并将新后和旧后指针上移
  • 新后与旧前:新后指针和旧前指针指向同一节点,如模型三第一次循环时新后与旧前都指向h('li', {key: 'A'}, ,当命中该规则时,更新该元素,然后将新后指向的元素移动到旧后的后方,同时移动指针(新后指针上移,旧前指针下移)
  • 新前与旧后:新前指针和旧后指针指向同一节点,如模型四第一次循环时新后与旧前都指向h('li', { key: 'C'}, ''),,当命中该规则时,更新该元素,然后将新前指向元素移动到旧前的前方,同时移动指针(新前指针下移,旧后指针上移)

循环方式

循环目的:将旧的DOM树变更为新的Vnode能生成的DOM,新DOM时目的,旧DOM是基础。 为了节约性能,需找到对应节点更新、删除、新建、移动以实现最小化更新。

循环条件:新/旧虚拟节点未处理完毕,即newStartIdx<=newEndIdx && oldStartIdx <= oldEndIdx

循环方式:四种命中依次比较

举个例子:

以模型一为例
在这里插入图片描述

在这里插入图片描述

第一次循环,新前旧前都指向h('li', {key: 'A'}, ''), 更新这个节点(patchVnode),及将key=A的文案变更为AAA,同时移动指针,即新前和旧前指针下移,指向h('li', { key: 'B'}, ''),,进入第二次循环。
在这里插入图片描述

第二次循环,新前和旧前指针都指向h('li', { key: 'B'}, ''),,更新节点,同时移动指针,即新前和旧前指针下移,旧前指向h('li', { key: 'C'}, ''),,新前指向h('li', { key: 'M'}, ''),,进入第三次循环。
在这里插入图片描述
在这里插入图片描述

第三次循环,新前与旧前指向节点不同,比较新后与旧后,发现都指向h('li', { key: 'C'}, ''),更新该节点,同时移动指针,即新后与旧后指针上移。此时旧后指针为1,旧前指针为2,不满足循环条件,结束循环。结束循环发现新前指针为2,新后指针为3 , 新前指针不大于新后指针,此时新前与新后之间的节点及为新增节点,这些新增节点需新增在 h('li', { key: 'B'}, '我是CCC'),前方,及新后节点后一个节点的前方。

以模型五为例:
在这里插入图片描述

在这里插入图片描述

模型五循环结束时,旧前<=旧后,旧前与旧后节点之间的节点都需要删除。

根据以上,我们可以得到基本的最小化更新函数:

import patchVnode from "./patchVnode"
import createElement from "./createElement"
// 判断是否是同一节点
function checkSameVnode(a, b) {
    return a.sel === b.sel && a.key === b.key
}
export default function updateChildren(parentElm, oldCh, newCh) {
    console.log('我是upadate')
    // 旧前
    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]
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // ① 新前与旧前
       if(checkSameVnode(oldStartVnode, newStartVnode)) {
            console.log('①')
           // 更新节点
            patchVnode(oldStartVnode, newStartVnode) 
           // 新前与旧前节点下移
            oldStartVnode = oldCh[++oldStartIdx] 
            newStartVnode = newCh[++newStartIdx]
        } 
        // ② 新后与旧后
        else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            console.log('②')
            // 更新节点
            patchVnode(oldEndVnode, newEndVnode)
            // 新后与旧后节点上移
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }
        // ③ 新后与旧前
        else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            console.log('③')
            // 更新节点
            patchVnode(oldStartVnode, newEndVnode)
            // 如果给定的子节点是对文档中现有节点的引用,insertBefore() 会将其从当前位置移动到新位置(在将节点附加到其他节点之前,不需要从其父节点删除该节点),若第二个参数为null,等同于appendChild()。
            // 把新后指向的vnode移动到旧后节点的后方
            parentElm.insertBefore(newEndVnode.elm, oldEndVnode.nextSibling?.elm)
            // 新后指针上移,旧前指针下移
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }
        // ④ 新前与旧后
        else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            console.log('④')
            // 更新节点
            patchVnode(oldEndVnode, newStartVnode)
            // 将新前指向的结点插入到旧前指向的节点前方
            parentElm.insertBefore(newStartVnode, oldStartVnode)
            // 新前指针下移,旧后指针上移
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } 
        // ⑤ 四种指针命中都未命中
        else {
            
        } 
    }
    // 新增节点->新节点仍有剩余
    if (newEndIdx >= newStartIdx) {
        let before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        for (let i = newStartIdx; i < newEndIdx + 1; i++) {
            parentElm.insertBefore(createElement(newCh[i]), before)
        }
    }

    // 删除节点->老节点仍有剩余
    if (oldEndIdx >= oldStartIdx) {
        for (let i = oldStartIdx; i < oldEndIdx + 1; i++) {
            parentElm.removeChild(oldCh[i].elm)
        }
    }
} 

在这里插入图片描述

四种命中都未命中

上文讲述了四种命中命中其一的处理场景,但是如模型6所示,我们会遇到四种命中都未命中的场景。针对这种场景,首先我们需要确定是否是新增节点,如果是就新增,如果不是就找到新前节点对应的旧节点并移动它到旧前节点的前方。

// key的MAP,降低循环的时间复杂度
let oldKeyToIdx
// 四种未命中时新节点在老节点中的位置
let idxInOld
// 四种未命中时新前节点需要移动到的位置
let elmToMove
// 构建第一次四个都未命中时未处理的老节点的key-index对象
// 经个人测试,如果存在的旧节点只是在头部复制了一份,由于缓存原因第二次进入会有bug,源码也有这个问题,但是缓存机制降低时间复杂度很牛逼,在这里保留,下方注释的方案可以解决这个bug
if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = oldKeyToIdx[newCh[newStartIdx].key]
if (isUndef(idxInOld)) {
    // 新前指向的节点在未处理的老节点当中没有,需要创建这个节点并插入到旧前指向节点的上方
    parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
    // 将新前节点下移
    newStartVnode = newCh[++newStartIdx]
} else if (isUndef(oldKeyToIdx[newCh[newEndIdx].key])) {
    // 新后指向的节点在未处理的老节点当中没有,需要创建这个节点并插入到旧后节点的后方
    parentElm.insertBefore(createElement(newEndVnode), oldEndVnode.elm.nextSibling)
    // 将新后节点上移
    newEndVnode = newCh[--newEndIdx]
} else {
    // 新前和新后节点在老节点中存在,只是被移动了位子,不在旧前和旧后指针指向的位置
    elmToMove = oldCh[idxInOld]
    if (elmToMove.sel !== newStartVnode.sel) {
        // 非同一节点,处理方式同新前指向节点在旧的虚拟节点中不存在
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
    } else {
        // 同一节点
        patchVnode(elmToMove, newStartVnode)
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
        // 防止vnode树与Dom不匹配,如相同位置仍存在相同的DOM,且置为undefined后循环遇到该虚拟节点可以跳过
        oldCh[idxInOld] = undefined
    }
    // 新前指针下移
    newStartVnode = newCh[++newStartIdx]
}
// 存储老虚拟节点未处理的节点key值的map
function createKeyToOldIdx(children, beginIdx, endIdx) {
    const map = {}
    for(let i = beginIdx; i < endIdx; i++) {
        const chVnode = children[i]
        const key = chVnode === null || chVnode === 0 ? 0 : chVnode.key
        map[key] = i
    }
    return map
}
// 判断一个数据是否为undefined
function isUndef(data) {
    return data === undefined
}

综合以上的信息,得到一份基本的最小化更新代码:

import patchVnode from "./patchVnode"
import createElement from "./createElement"
// 判断是否是同一节点
function checkSameVnode(a, b) {
    return a.sel === b.sel && a.key === b.key
}
// 存储老虚拟节点未处理的节点key值的map
function createKeyToOldIdx(children, beginIdx, endIdx) {
    const map = {}
    for(let i = beginIdx; i < endIdx; i++) {
        const chVnode = children[i]
        const key = chVnode === null || chVnode === 0 ? 0 : chVnode.key
        map[key] = i
    }
    return map
}
// 判断一个数据是否为undefined
function isUndef(data) {
    return data === undefined
}
export default function updateChildren(parentElm, oldCh, newCh) {
    console.log('我是upadate')
    // 旧前
    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]
    // key的MAP,降低循环的时间复杂度
    let oldKeyToIdx
    // 四种未命中时新节点在老节点中的位置
    let idxInOld
    // 四种未命中时新前节点需要移动到的位置
    let elmToMove
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 移除该节点,不需要比较直接跳过
        if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
        }
        else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
        }
        else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
        }
        else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
        }
        // ① 新前与旧前
        else if(checkSameVnode(oldStartVnode, newStartVnode)) {
            // 比对同一节点
            console.log('①')
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } 
        // ② 新后与旧后
        else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            console.log('②')
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }
        // ③ 新后与旧前
        else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            console.log('③')
            patchVnode(oldStartVnode, newEndVnode)
            // 把新后指向的vnode移动到旧后节点的后方
            // 如果给定的子节点是对文档中现有节点的引用,insertBefore() 会将其从当前位置移动到新位置(在将节点附加到其他节点之前,不需要从其父节点删除该节点)。
            parentElm.insertBefore(newEndVnode.elm, oldEndVnode.nextSibling?.elm)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }
        // ④ 新前与旧后
        else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            console.log('④')
            patchVnode(oldEndVnode, newStartVnode)
            // 将新前指向的结点插入到旧前指向的节点前方
            parentElm.insertBefore(newStartVnode, oldStartVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } 
        else {
            console.log('⑤')
            // 都未命中
            // 构建第一次四个都未命中时未处理的老节点的key-index对象
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            }
            // oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = oldKeyToIdx[newCh[newStartIdx].key]
            if (isUndef(idxInOld)) {
                // 新前指向的节点在未处理的老节点当中没有,需要创建这个节点并插入到旧前指向节点的上方
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
                // 将新前节点下移
                newStartVnode = newCh[++newStartIdx]
            } else if (isUndef(oldKeyToIdx[newCh[newEndIdx].key])) {
                // 新后指向的节点在未处理的老节点当中没有,需要创建这个节点并插入到旧后节点的后方
                parentElm.insertBefore(createElement(newEndVnode), oldEndVnode.elm.nextSibling)
                // 将新后节点上移
                newEndVnode = newCh[--newEndIdx]
            } else {
                // 新前和新后节点在老节点中存在,只是被移动了位子,不在旧前和旧后指针指向的位置
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    // 非同一节点
                    parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
                } else {
                    // 同一节点
                    patchVnode(elmToMove, newStartVnode)
                    parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
                    // 防止vnode树与Dom不匹配,如相同位置仍存在相同的DOM
                    oldCh[idxInOld] = undefined
                }
                // 新前指针下移
                newStartVnode = newCh[++newStartIdx]
            }
        } 
    }
    // 新增节点->新节点仍有剩余
    if (newEndIdx >= newStartIdx) {
        let before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        for (let i = newStartIdx; i < newEndIdx + 1; i++) {
            parentElm.insertBefore(createElement(newCh[i]), before)
        }
    }

    // 删除节点->老节点仍有剩余
    if (oldEndIdx >= oldStartIdx) {
        for (let i = oldStartIdx; i < oldEndIdx + 1; i++) {
            parentElm.removeChild(oldCh[i].elm)
        }
    }
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值