快速Diff算法
预处理
前面讲到简单Diff算法和双端Diff算法,它们使用不一样的对比规则对虚拟节点的 type(元素名)和 虚拟节点的key(唯一标识)来区分是否有可以复用的旧节点。快速Diff算法也是一样的,不过要比简单Diff和双端Diff多了一步预处理的操作。
什么是预处理
什么是预处理呢?用文本来举个例子:
const TEXT1 = 'I am a front-end developer'
const TEXT2 = 'I am a back-end developer'
要对这两段文本进行diff,首先会对它进行全等比较:if (TEXT1 === TEXT2) return
,如果全等旧没有必要进入核心的diff步骤了。除了全等比较,还会对他们进行前缀于后缀的比较。一眼就能看到这两段文本的头部和尾部分别有一段相同的内容。
I am a front -end developer
I am a back -end developer
对于相同的内容,不需要进行Diff操作,因此对于 TEXT1 和 TEXT2 来说,真正需要Diff操作的部分是:
TEXT1:front
TEXT2:back
这是一种简化问题的方式,好处是可以在特定情况下能够轻松判断文本的插入和删除。
预处理便是将上面的例子掐头去尾的过程,那么再看另外一个例子:
const TEXT3 = I like you
const TEXT4 = I like you too
这两段文本经过预处理之后可以得到:
TEXT3:
TEXT4:too
如果 TEXT3 是新的内容,那么只需要删除多余的 too 就可以完成文本更新;否则,添加 too 完成文本更新。
预处理要怎么做 - 例一
快速Diff算法就是借鉴了纯文本的diff算法中预处理的步骤。以下面的两组节点为例:
const oldChildren = [{ type: 'p', key: '1' },{ type: 'p', key: '2' },{ type: 'p', key: '3' }
]
const newChildren = [{ type: 'p', key: '1' },{ type: 'p', key: '4' },{ type: 'p', key: '2' },{ type: 'p', key: '3' }
]
从图中可以看到,两组节点具有相同的前置节点 p - 1,以及相同的后置节点 p - 2、p - 3。对于相同的前置节点和后置节点,由于它们在新旧两组子节点中的相对位置不变,所以不需要移动它们,但仍然要在它们之间打补丁。
处理前置节点
对于前置节点,可以建立索引 j ,初始值为0,指向两组子节点的开头。开启一个while循环,让索引 j 递增,直至遇到不同的节点为止。
function patchKeyedChildren (n1, n2, container) {const newChildren = n1.childrenconst oldChildren = n2.children// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0let oldVNode = oldChildren[j]let newVNode = newChildren[j]// while 循环向后遍历,直到遇到不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container)// 让索引 j 递增以对下一个节点进行处理j++oldVNode = oldChildren[j]newVNode = newChildren[j]}
}
上面使用while循环查找所有相同的前置节点,并调用patch函数进行打补丁,直到遇到key值不同的节点为止。这样就完成了对前置节点的预处理。
处理后置节点
接下来就要处理后置节点,因为新旧两组子节点的数量不同所以还需要两个索引,指向新旧两组子节点的最后一个节点。然后再开启一个while循环从后向前遍历这两组子节点,直到遇到key值不同的节点为止。
function patchKeyedChildren (n1, n2, container) {const newChildren = n1.childrenconst oldChildren = n2.children// 处理相同的前置节点// 索引 j 指向新旧两组子节点的开头let j = 0let oldVNode = oldChildren[j]let newVNode = newChildren[j]// while 循环向后遍历,直到遇到不同 key 值的节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container)// 让索引 j 递增以对下一个节点进行处理j++oldVNode = oldChildren[j]newVNode = newChildren[j]}// 获取最后的子节点的索引值let oldEnd = oldChildren.length - 1let newEnd = newChildren.length - 1// 获取最后的子节点oldVNode = oldChildren[oldEnd]newVNode = newChildren[newEnd]// while 循环从后向前遍历,直至遇到不同 key 值得节点为止while (oldVNode.key === newVNode.key) {// 调用 patch 函数进行更新patch(oldVNode, newVNode, container)// 让索引 j 递减以对下一个节点进行处理(因为是从后往前所以递减)oldEnd--newEnd--oldVNode = oldChildren[oldEnd]newVNode = newChildren[newEnd]}
}
与处理相同得前置节点一样,在while循环内,需要调用patch函数进行打补丁,然后递减两个索引oldEnd、newEnd。
新增节点
从图中可以看到,相同的前置节点和后置节点被处理完之后,旧的一组一节点全部被处理了,而在新的一组子节点中,还有一个没有被处理的节点 p - 4。因此得出,p - 4 是一个新增节点:
oldEnd < j
成立,说明在预处理时,所有旧子节点都处理完毕了
newEnd >= j
成立,说明预处理后,新的一组子节点中,存在未被处理的节点,这些节点就是新增的节点
索引值在 j 和 newEnd 之间的任何节点都需要作为新的子节点进行挂载,挂载新元素就要找到正确的锚点元素。从上图中看到,新增节点应该挂载到节点 p - 2 所对应的真实DOM前面,所以将 p - 2 作为挂载操作的锚点元素。
function patchKeyedChildren (n1, n2, c