图文设计成都网站搜索排名优化公司

当前位置: 首页 > news >正文

图文设计,成都网站搜索排名优化公司,三维家在线设计官网,网站服务器有哪些类型有哪些类型有哪些类型有哪些类型我们介绍了简单 Diff 算法的实现原理。简单 Diff 算法利用虚拟节点的 key 属性#xff0c;尽可能地复用 DOM元素#xff0c;并通过移动 DOM的方式来完成更新#xff0c;从而减少不断地创建和销毁 DOM 元素带来的性能开销。但是#xff0c;简单 Diff 算法仍然存在很多缺陷尽可能地复用 DOM元素并通过移动 DOM的方式来完成更新从而减少不断地创建和销毁 DOM 元素带来的性能开销。但是简单 Diff 算法仍然存在很多缺陷这些缺陷可以通过本章将要介绍的双端 Diff 算法解决。 1.双端比较的原理 双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。因此我们需要四个索引值分别指向新旧两组子节点的端点.如下图 双端比较的方式 在双端比较中每一轮比较都分为四个步骤如图 10-5 中的连线所示。 比较的过程如下描述 第一步: 比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 p-4看看它们是否相同。由于两者的key 值不同因此不相同不可复用于是什么都不做。 第二步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的最后一个子节点 p-3看看它们是否相同。由于两者的 key 值不同因此不相同不可复用于是什么都不做。 第三步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的最后一个子节点 p-3看看它们是否相同。由于两者的 key 值不同因此不相同不可复用于是什么都不做。 第四步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的第一个子节点 p-4。由于它们的 key 值相同因此可以进行 DOM 复用。 function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container) }function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx]if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]}}第一轮 DOM 移动操作完成 态状的点节后新旧两组子节点以及真实 DOM 节点的状态如下
此时真实 DOM 节点顺序为 p-4、p-1、p-2、p-3这与新的 一组子节点顺序不一致。这是因为diff算法还没结束还需要进行下一轮更新。因此我们需要将更新逻辑封装到一个 while 循环中 function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx] while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]} }}由于在每一轮更新完成之后紧接着都会更新四个索引中与当前更新轮次相关联的索引所以整个 while 循环执行的条件是:头部索引值要小于等于尾部索引值。 在第一轮更新结束后循环条件仍然成立因此需要进行下一轮的比较 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-2看看它们是否相同。由于两者的 key 值不 同不可复用所以什么都不做。 这里我们使用了新的名词: 。它指的是头部索引oldStartIdx 和 newStartIdx 所指向的节点。 第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3两者的 key 值相同可以复用。另外由于两者都处于尾部因此不需要对真实 DOM 进行移动操作只需要打补丁即可: function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx]while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁 patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量 oldEndVNode oldChildren[–oldEndIdx] newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]}}}真实 DOM 的顺序相比上一轮没有变化因为在这一轮的比较中没有对 DOM 节点进行移动只是对 p-3 节点打补丁。接下来我们再根据图 上图所示的状态执行下一轮的比较 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-2看看它们是否相同。由于两者的 key 值不 同不可复用因此什么都不做。 第二步:比较旧的一组子节点中的尾部节点 p-2 与新的一组子节点中的尾部节点 p-1看看它们是否相同由于两者的 key 值不 同不可复用因此什么都不做。 第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-1。两者的 key 值相同可以复用。 在第三步的比较中我们找到了相同的节点这说明: p-1原本是头部节点但是在新的顺序中它变成了尾部节点。因此我们需要将节点p-1对应的真实 DOM 移动到旧的一组子节点的尾部节点 p-2 所对应的真实 DOM 后面同时还需要更新相应的索引到下一个位置如图 下图所示
function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx]while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较 patch(oldStartVNode, newEndVNode, container) insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling) oldStartVNode oldChildren[oldStartIdx] newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]}}}下一轮循环 第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组 子节点中的头部节点 p-2。发现两者 key 值相同可以复用。但 两者在新旧两组子节点中都是头部节点因此不需要移动只需 要调用 patch 函数进行打补丁即可。 function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx]while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁 patch(oldStartVNode, newStartVNode, container)// 更新相关索引指向下一个位置 oldStartVNode oldChildren[oldStartIdx] newStartVNode newChildren[newStartIdx]} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode oldChildren[oldStartIdx]newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]}}}在这一轮更新之后新旧两组子节点与真实 DOM 节点的状态如图下图 10-10 所示。 双端比较的优势 优势减少移动操作。 案例分析如下图的新旧两组子节点
简单diff移动两次
双端diff移动一次 非理想状态的处理方式 第一轮都无法命中 旧的一组子节点:p-1、p-2、p-3、p-4。新的一组子节点:p-2、p-4、p-1、p-3。 当我们尝试按照双端 Diff 算法的思路进行第一轮比较时会发现无法命中四个步骤中的任何一步。这个时候怎么办呢这时我们只能通过增加额外的处理步骤来处理这种非理想情况。既然两个头部和两个尾部的四个节点中都没有可复用的节点那么我们就尝试看看非头部、非尾部的节点能否复用。具体做法是拿新的一组子节点中的头部节点去旧的一组子节点中寻找如下面的代码 while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引指向下一个位置oldStartVNode oldChildren[oldStartIdx]newStartVNode newChildren[newStartIdx]} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode oldChildren[oldStartIdx]newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]} else {// 处理非理想情况// 在旧的一 组子节点中找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引 const idxInOld oldChildren.findIndex(node node.key newStartVNode.key)}} 如下图在旧子节点中寻找可复用节点
function patchChildren(n1, n2, container) {patchKeyedChildren(n1, n2, container)}function patchKeyedChildren(n1, n2, container){const oldChildren n1.children const newChildren n2.children// 四个索引值let oldStartIdx 0let oldEndIdx oldChildren.length - 1let newStartIdx 0let newEndIdx newChildren.length - 1// 四个索引指向的 vnode 节点let oldStartVNode oldChildren[oldStartIdx]let oldEndVNode oldChildren[oldEndIdx]let newStartVNode newChildren[newStartIdx]let newEndVNode newChildren[newEndIdx]while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引指向下一个位置oldStartVNode oldChildren[oldStartIdx]newStartVNode newChildren[newStartIdx]} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode oldChildren[oldStartIdx]newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]} else {// 处理非理想情况// 在旧的一 组子节点中找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld oldChildren.findIndex(node node.key newStartVNode.key)// idxInOld 大于 0说明找到了可复用的节点并且需要将其对应的真实DOM 移动到头部 if(idxInOld 0) { // idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁 patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前因此使用后者作为锚点insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处因此将其设置为 undefined oldChildren[idxInOld] undefined// 最后更新 newStartIdx 到下一个位置 newStartVNode newChildren[newStartIdx]}}}}在上面这段代码中首先判断 idxInOld 是否大于 0。如果条件 成立则说明找到了可复用的节点然后将该节点对应的真实 DOM 移 动到头部。为此我们先要获取需要移动的节点这里的 oldChildren[idxInOld] 所指向的节点就是需要移动的节点。在移 动节点之前不要忘记调用 patch 函数进行打补丁。接着调用 insert 函数并以现在的头部节点对应的真实 DOM 节点 oldStartVNode.el 作为锚点参数来完成节点的移动操作。当节点移 动完成后还有两步工作需要做 由于处于 idxInOld 处的节点已经处理过了(对应的真实 DOM 移到了别处)因此我们应该将 oldChildren[idxInOld] 设 置为undefined。 新的一组子节点中的头部节点已经处理完毕因此将 newStartIdx 前进到下一个位置。
经过上述两个步骤的操作后新旧两组子节点以及真实 DOM 节点 的状态如图 下图所示 此时真实 DOM 的顺序为:p-2、p-1、p-3、p-4。接着双端 Diff 算法会继续进行。如下图所示
第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4两者 key 值不同不可复用。 第二步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的尾部节点 p-3两者 key 值不同不可复用。 第三步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的尾部节点 p-3两者 key 值不同不可复用。 第四步:比较旧的一组子节点中的尾部节点 p-4 与新的一组子节点中的头部节点 p-4两者的 key 值相同可以复用。 在这一轮比较的第四步中我们找到了可复用的节点。因此按照双端 Diff 算法的逻辑移动真实 DOM即把节点 p-4 对应的真实DOM 移动到旧的一组子节点中头部节点 p-1 所对应的真实 DOM 前面如图 下图 所示 此时真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着开始下一轮的比较 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1两者的 key 值相同可以复用。 在这一轮比较中第一步就找到了可复用的节点。由于两者都处于头部所以不需要对真实 DOM 进行移动只需要打补丁即可。在这一步操作过后新旧两组子节点与真实 DOM 节点的状态如图 下图 所示
此时真实 DOM 节点的顺序是:p-2、p-4、p-1、p-3。接着进行下一轮的比较。需要注意的一点是此时旧的一组子节点的 头部节点是 undefined。这说明该节点已经被处理过了因此不需要再处理它了直接跳过即可。为此我们需要补充这部分逻辑的代码具体实现如下: while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {// 增加两个判断分支如果头尾部节点为 undefined则说明该节点已经被处理过了直接跳到下一个位置 if (!oldStartVNode) { oldStartVNode oldChildren[oldStartIdx] } else if (!oldEndVNode) { oldEndVNode oldChildren[–oldEndIdx] }else if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引指向下一个位置oldStartVNode oldChildren[oldStartIdx]newStartVNode newChildren[newStartIdx]} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode oldChildren[oldStartIdx]newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]} else {// 处理非理想情况// 在旧的一 组子节点中找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld oldChildren.findIndex(node node.key newStartVNode.key)// idxInOld 大于 0说明找到了可复用的节点并且需要将其对应的真实DOM 移动到头部if(idxInOld 0) {// idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前因此使用后者作为锚点insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处因此将其设置为 undefinedoldChildren[idxInOld] undefined// 最后更新 newStartIdx 到下一个位置newStartVNode newChildren[newStartIdx]}} }观察上面的代码在循环开始时我们优先判断头部节点和尾部节点是否存在。如果不存在则说明它们已经被处理过了直接跳到下一个位置即可。在这一轮比较过后新旧两组子节点与真实 DOM 节点的状态如图 下图 所示
现在四个步骤又重合了接着进行最后一轮的比较 第一步:比较旧的一组子节点中的头部节点 p-3 与新的一组子节点中的头部节点 p-3两者的 key 值相同可以复用。在第一步中找到了可复用的节点。由于两者都是头部节点因此不需要进行 DOM 移动操作直接打补丁即可。在这一轮比较过后最终状态如图 下图 所示 这时满足循环停止的条件于是更新完成。最终真实 DOM 节点的顺序与新的一组子节点的顺序一致都是:p-2、p-4、p-1、p-3。 添加新元素 添加新元素的时机1.四个步骤的比较中都找不到可复用的节点 。 2.尝试拿新的一组子节点中的头部节点 p-4 去旧的一组子节点中寻找具有相同 key 值的节点但在旧的一组子节点中根本就没有 p-4 节点。这说明节点 p-4 是一个新增节点。 案例1如下 旧的一组子节点:p-1、p-2、p-3。新的一组子节点:p-4、p-1、p-3、p-2。
代码如下 while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {if (!oldStartVNode) {oldStartVNode oldChildren[oldStartIdx]} else if (oldStartVNode.key newStartVNode.key) {// 步骤一:oldStartVNode 和 newStartVNode 比较// 调用 patch 函数在 oldStartVNode 与 newStartVNode 之间打补丁patch(oldStartVNode, newStartVNode, container)// 更新相关索引指向下一个位置oldStartVNode oldChildren[oldStartIdx]newStartVNode newChildren[newStartIdx]} else if (oldEndVNode.key newEndVNode.key) {// 步骤二:oldEndVNode 和 newEndVNode 比较// 节点在新的顺序中仍然处于尾部不需要移动但仍需打补丁patch(oldEndVNode, newEndVNode, container)// 更新索引和头尾部节点变量oldEndVNode oldChildren[–oldEndIdx]newEndVNode newChildren[–newEndIdx]} else if(oldStartVNode.key newEndVNode.key) {// 步骤三:oldStartVNode 和 newEndVNode 比较patch(oldStartVNode, newEndVNode, container)insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)oldStartVNode oldChildren[oldStartIdx]newEndVNode newChildren[–newEndIdx]} else if (oldEndVNode.key newStartVNode.key) {// 我们找到了具有相同 key 值的节点。这说明原来处于尾部的节点在新的顺序中应该处于头部。// 于是我们只需要以头部元素oldStartVNode.el 作为锚点将尾部元素 oldEndVNode.el 移动到锚点前面即可。// 但需要注意的是在进行 DOM 的移动操作之前仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。// 第四步:oldEndVNode 和 newStartVNode 比较// 仍然需要调用 patch 函数进行打补丁patch(oldEndVNode, newStartVNode, container)// 移动dom操作 oldEndVNode.el 移动到 oldStartVNode.el 前面insert(oldEndVNode.el, container, oldStartVNode.el)// 移动 DOM 完成后更新索引值指向下一个位置oldEndVNode oldChildren[–oldEndIdx]newStartVNode newChildren[newStartIdx]} else {// 处理非理想情况// 在旧的一 组子节点中找到与新的一组子节点的头部节点具有相同 key 值的节点// 遍历旧的一组子节点试图寻找与 newStartVNode 拥有相同 key 值的节点// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引const idxInOld oldChildren.findIndex(node node.key newStartVNode.key)// idxInOld 大于 0说明找到了可复用的节点并且需要将其对应的真实DOM 移动到头部if(idxInOld 0) {// idxInOld 位置对应的 vnode 就是需要移动的节点const vnodeToMove oldChildren[idxInOld]// 不要忘记除移动操作外还应该打补丁patch(vnodeToMove, newStartVNode, container)// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前因此使用后者作为锚点insert(vnodeToMove.el, container, oldStartVNode.el)// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到了别处因此将其设置为 undefinedoldChildren[idxInOld] undefined// 最后更新 newStartIdx 到下一个位置newStartVNode newChildren[newStartIdx]} else { // 新增节点// 将 newStartVNode 作为新节点挂载到头部使用当前头部节点oldStartVNode.el 作为锚点patch(null, newStartVNode, container, oldStartVNode.el)}}}当条件idxInOld 0不成立时说明 newStartVNode 节点是全新的节点。又由于 newStartVNode 节点 是头部节点因此我们应该将其作为新的头部节点进行挂载。所以 在调用 patch 函数挂载节点时我们使用 oldStartVNode.el 作为 锚点。在这一步操作完成之后新旧两组子节点以及真实 DOM 节点的 状态如下图所示
案例2 旧的一组子节点:p-1、p-2、p-3。新的一组子节点:p-4、p-1、p-2、p-3。 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4两者的 key 值不同不可以复用。 第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3两者的 key 值相同可以复用。 在第二步中找到了可复用的节点因此进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图下图 所示 接着进行下一轮的比较 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4两者的 key 值不同不可以复用。 第二步:比较旧的一组子节点中的尾部节点 p-2 与新的一组子节点中的尾部节点 p-2两者的 key 值相同可以复用。 我们又在第二步找到了可复用的节点于是再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示 接着进行下一轮的更新 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-4两者的 key 值不同不可以复用。 第二步:比较旧的一组子节点中的尾部节点 p-1 与新的一组子节点中的尾部节点 p-1两者的 key 值相同可以复用。 还是在第二步找到了可复用的节点再次进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图 所示
当这一轮更新完毕后由于变量 oldStartIdx 的值大于oldEndIdx 的值满足更新停止的条件因此更新停止。但通过观察可知节点 p-4 在整个更新过程中被遗漏了没有得到任何处理这说明我们的算法是有缺陷的。为了弥补这个缺陷我们需要添加额外的处理代码 while (oldStartIdx oldEndIdx newStartIdx newEndIdx) { // 省略部分代码}// 循环结束后检查索引值的情况if (oldEndIdx oldStartIdx newStartIdx newEndIdx) {// 如果满足条件则说明有新的节点遗留需要挂载它们for (let i newStartIdx; i newEndIdx; i) {patch(null, newChildren[i], container, oldStartVNode.el) }}我们在 while 循环结束后增加了一个 if 条件语句检查四个索引值的情况。根据图上图可知如果条件oldEndIdx oldStartIdx newStartIdx newEndIdx成立说明新的一组子节点中有遗留的节点需要作为新节点挂载。哪些节点是新节点呢?索引值位于 newStartIdx 和 newEndIdx 这个区间内的节点都是新节点。于是我们开启一个 for 循环来遍历这个区间内的节点并逐一挂载。挂载时的锚点仍然使用当前的头部节点oldStartVNode.el这样就完成了对新增元素的处理。 移除不存在的元素 案例如下 旧的一组子节点:p-1、p-2、p-3。新的一组子节点:p-1、p-3。
可以看到在新的一组子节点中 p-2 节点已经不存在了。为了搞清楚应该如何处理节点被移除的情况我们还是按照双端 Diff 算法的思路执行更新。 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-1两者的 key 值相同可以复用。 在第一步的比较中找到了可复用的节点于是执行更新。在这一轮比较过后新旧两组子节点以及真实 DOM 节点的状态如图下图所示 接着执行下一轮更新 第一步:比较旧的一组子节点中的头部节点 p-2 与新的一组子节点中的头部节点 p-3两者的 key 值不同不可以复用。 第二步:比较旧的一组子节点中的尾部节点 p-3 与新的一组子节点中的尾部节点 p-3两者的 key 值相同可以复用。 在第二步中找到了可复用的节点于是进行更新。更新后的新旧两组子节点以及真实 DOM 节点的状态如图 下图所示 此时变量 newStartIdx 的值大于变量 newEndIdx 的值满足更新停止的条件于是更新结束。但观察图 10-34 可知旧的一组子节点中存在未被处理的节点应该将其移除。因此我们需要增加额外的代码来处理它如下所示: while (oldStartIdx oldEndIdx newStartIdx newEndIdx) { // 省略部分代码}// 循环结束后检查索引值的情况 if (oldEndIdx oldStartIdx newStartIdx newEndIdx) {// 如果满足条件则说明有新的节点遗留需要挂载它们for (let i newStartIdx; i newEndIdx; i) {patch(null, newChildren[i], container, oldStartVNode.el)}} else if (newEndIdx newStartIdx oldStartIdx oldEndIdx) {for (let i oldStartIdx; i oldEndIdx; i) {unmount(oldChildren[i])}}与处理新增节点类似我们在 while 循环结束后又增加了一个else…if 分支用于卸载已经不存在的节点。由图 上图 可知索引值位于 oldStartIdx 和 oldEndIdx 这个区间内的节点都应该被卸载于是我们开启一个 for 循环将它们逐一卸载。