双端比较

10 双端比较

10.1 双端比较的原理

双端比较通过定义新旧节点的开始下标与结束下标四个变量。然后进行对比

  1. 新节点开始下标的节点和旧节点开始下标的节点对比
  2. 新节点结束下标的节点和旧节点结束下标的节点对比
  3. 新节点结束下标的节点和旧节点开始下标的节点对比
  4. 新节点开始下标的节点和旧节点结束下标的节点对比
10.1.1

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function patchKeyedChildren(n1,n2,container){
const oldChildren = n1.children
const newChildren = n2.children
// 四个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 四个索引指向的vnode节点
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
while(oldStarIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(oldStartVNode.key === newStartVNode.key){
patch(oldEndVNode,newEndVNode,container)
// 不需要移动dom 移动下标和元素即可
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if(oldEndVNode.key === newEndVNode.key) {
patch(oldEndVNode,newEndVNode,container)
// 也不需要移动
oldEndVNode = oldChildren[--oldStartIdx]
oldStartVNode = newChildren[--newStartIdx]
} else if(oldStartVNode.key === newEndVNode.key) {
// 进行patch
patch(oldStartVNode,newEndVNode,container)
// 因为旧的开始变为新的结束 因此需要将
insert(oldStartVNode,container,oldEndVNode.el.nextSibing)
// 移动下标 更新元素
oldStartVNode = oldChildren[++oldStartIdx]
} else if(oldEndVNode.key === newStartVNode.key) {
// 先进行patch
patch(oldEndVNode,newStartVNode,container)
// 再进行移动DOM操作
// 因为旧的最后一个变成新的第一个,因为需要移动到第一个元素前面
insert(oldEndVNode.el, container, oldStartVNode.el)

//移动下标和元素
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
}
}
}

处理新增和删除情况

  1. 处理在循环内部

    在while循环中增加一个else处理在新节点在旧节点的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
    if(idxInOld > 0) {
    patch(oldChildren[idxInOld],newStartVNode,container)
    // 将元素移动到最前面
    insert(oldChildren[idxInOld].el,container, oldStartVNode.el)
    // 由于idxInOld 对应元素已经移动 所以需要将原来的位置对应元素置为undefined
    oldChildren[idxInOld] = undefined
    newStartVNode = newChildren[++newStartIdx]
    } else {
    // 将newStartVNode作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el作为锚点
    patch(null,newStartVNode,container)
    newStartVNode = newChildren[++newStartIdx]
    }

    因为旧节点可能出现undefined情况,因此需要跳过旧节点为假的情况,需要在循环内最前面添加判断

    1
    2
    3
    4
    5
    if(!oldStartVNode) {
    oldStartVNode = oldChildren[++oldStartIdx]
    } else if(!oldEndVNode) {
    oldEndVNode = oldChildren[--oldEndIdx]
    }
  2. 处理循环外部的新增和删除情况

    首先看新增情况,如果新节点开始下标小于新节点结束下标,因此在循环后面增加判断

    1
    2
    3
    4
    5
    if(newStartIdx <= newEndIdx && oldStartIdx > oldEndIdx) {
    for(let i = newStartIdx; i <= newEndIdx;i++) {
    patch(null,newChildren[i],container, oldStartVNode.el)
    }
    }

    接着处理删除情况,如果旧节点开始下标小于旧节点技术下标,需要把多余的节点删除,因此增加判断

    1
    2
    3
    4
    5
    else if(oldStartIdx <= oldEndIdx && newStartIdx > newEndIdx) {
    for(let i = oldStartIdx; i <= oldEndIdx, i++) {
    unmount(oldChildren[i])
    }
    }