简单Diff算法让然存在很多缺陷,这些缺陷可以通过本章的双端Diff算法解决。
10.1 双端比较的原理
简单Diff算法的问题在于,它对DOM的移动操作并不是最优的。
使用简单Diff算法,则会发生两次DOM移动操作。
但是其实只需要一次DOM移动操作即可完成更新。
接下来就来讨论双端Diff算法的原理。
图10-4 四个索引值,分别指向新旧两组子节点的端点。
代码创建四个索引值,分别是newStartIdx和newEndIdx、oldStartIdx和oldEndIdx。
有了索引值,就可以找到它所指向的虚拟节点了。oldStartVNode和oldEndVNode是旧的一组子节点中的第一个节点和最后一个节点。newStartVNode和newEndVNode则是新的一组子节点中的第一个节点和最后一个节点。
图10-5 双端比较的方式
双端比较中,每一轮比较都分为四个步骤。
1.比较旧的一组子节点中的第一个子节点p-4与新的一组子节点的第一个子节点p-1。它们是否相同?是否可复用?
2.比较旧的一组子节点中的最后一个子节点p-3与新的一组子节点的最后一个子节点p-4。它们是否相同?是否可复用?
3.比较旧的一组子节点中的第一个子节点p-3与新的一组子节点中的最后一个子节点p-1。它们是否相同?是否可复用?
4.比较旧的一组子节点中的最后一个子节点p-4与新的一组子节点中的第一个子节点p-4。它们是否相同?是否可复用?
对于可复用的DOM节点,我们只需要通过DOM移动操作完成更新即可。那么应该如何移动DOM元素呢?
对于第四步中,节点p-4原本是最后一个子节点,但在新的顺序中,它变成了第一个子节点。换句话说是什么?
patchKeyedChildren函数代码流程图如下:
请画出移动后的新旧两组子节点预计真实DOM节点的状态。
在这一步DOM的移动操作完成后,接下来是比较关键的步骤,即更新索引值。
第一步移动之后,Diff算法还没结束,还需要下一轮更新。因此需要将更新逻辑封装到while循环中。while循环执行的条件是:头部索引值要小于等于尾部索引值。
第二轮的比较。
请画出移动后的新旧两组子节点预计真实DOM节点的状态。
第三轮比较。
第四轮比较。
10.2 双端比较的优势
图10-11 新旧两组子节点
1.使用简单Diff算法;
2.使用双端Diff算法;
10.3 非理想状况的处理方式
双端Diff算法的每一轮比较的过程都分为四个步骤。每一轮比较都会命中四个步骤之一,这是非常理想的情况。并非所有情况都这么理想。
第一轮比较,会无法命中四个步骤的任何一步。应该怎么办呢?
说明两个头部和两个尾部的四个节点中都没有可复用的节点,那么就尝试看看非头部、非尾部的节点能否复用。
// 省略前面四次比较
// 遍历旧的一组子节点,寻找与newStartVNode拥有相同key值的节点
// 用一个变量存储,newStartVNode在旧的一组子节点中的索引
在旧的一组子节点中,找到与新的一组子节点的头部节点具有相同key值的节点意味着什么?
请画出经过上述操作后的新旧两组子节点以及真实DOM的状态图。
接着双端Diff算法会继续进行。也就是第二轮比较。
请画出第二轮比较后的新旧两组子节点以及真实DOM的状态图。
第三轮比较。
请画出第三轮比较后的新旧两组子节点以及真实DOM的状态图。
第四轮比较。 旧的头部节点是undefined,直接跳过。
代码实现: 增加两个判断条件。
第五轮比较。
请画出第五轮比较后的新旧两组子节点以及真实DOM的状态图。
10.4 添加新元素
在上一节中,我们会拿新的一组子节点中的头部节点去旧的一组节点中寻找可复用的节点,然而并非总是能找到。
图10-25 新增节点的情况
首先第一轮比较。在这四个步骤里找不到可复用的节点。于是拿头部节点p-4去旧的一组子节点里找,也找不到。
这说明p-4是新增节点,那么它应该挂载到哪呢?
// 在while循环中
// 省略前面四个比较的步骤
// 最后的else中
// 如果新头部节点在旧的一组子节点中找不到
// 新增节点挂载到头部
我们再来看另外一个例子。
图10-28 新旧两组子节点以及真实DOM节点的状态
会发现什么问题?该怎么解决呢?
// 在while循环之后
// 还需要判断是否有新节点需要挂载
if(?){
// 挂载新节点
}
10.5 移除不存在的元素
图10-32 移除节点的情况
添加移除节点的逻辑。
// 在while循环之后
// 还需要判断是否有节点需要移除
if(?){
// 移除节点
}