这里实现 Element更新中 children为 Array -> Array 的逻辑
Array1 -> Array2 之前的对比,不单单考虑删除老的 Array, 创建新的 Array而已,需要考虑更过的场景来适配 Element 的更新 , 因为在前端领域中,往往页面中数据发生的改变,都是Element 节点中间的一小部分。
例如
- Element 节点中, 只有一个数据发生变化 ,不可能遍历全部Array来查找改变的数据,这样开销太大,性能也不行
所以Vue实现的 diff 算法,用于适配 Element更新场景, 找出Element 中发生的改变, 进行Element 的更新
Array -> Array的几种情况
使用双端对比算法 Array -> Array
双端对比算法的核心: 就是筛选出 两个 Array 中间的 乱序元素
6.1 左侧对比

1. 左侧
- Array1: A B C
- Array2: A B D E
- 找出左侧相同的元素, 即 A B,得到乱序的元素 D E ,然后基于乱序元素进行对比, 确定要改变的元素 C -> D E
happy patch
/* ArrayToArray.js*/// 1. 左侧对比// ( A B ) C// ( A B ) D Econst prevChildren = [// 多谢了一个 Keyh("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "D" }, "D"),h("p", { key: "E" }, "E"),];// 导出手动改变的的组件export default {name: "ArrayToArray",setup() {// 定义响应式变量const isChange = ref(false)window.isChange = isChangereturn {isChange}},render() {const self = this// 实现根据 isChange 判断显示的 节点return self.isChange === true ? h("div", {}, nextChildren) : h("div", {}, prevChildren)}};

接上节 Element 更新的变化, Array -> Array 的逻辑
/* renderer.ts *//* 其他代码 */if (shapeFlag & ShapeFlags.TEXT_CHILDREN){// 其他代码} else { // 更新后的节点是一个 Arrayif (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// Text -> Array// 1. 把Text清空}else {// Array -> Array// 老Array -> 新Array// 使用 patchKeyChildren 函数实现// 传入 老的 Array 和 新的 Array, container, parentComponet 之后patch()渲染函数会需要patchKeyChildren(c1, c2, container, parentComponet)}}
实现左侧对比的逻辑
实现对左侧对比的思路
实现思路:1. 基于指针 i e1 e2- 其中 i 指针为 0 , 指向两个数组的头部- e1 为 老节点的 尾指针, 值为 e1 = n1.children.length - 1 Array1 的元素个数- e2 为新节点的 尾指针, 值尾 e2 = n2.children.length - 1 Array2 的元素个数2. 基于左侧对比, n1.children [i] 于 n2.children [i] 比较- 左侧元素相同, 把 i 指针向后一个位 移动 i++- 当两个Array元素不同时,i 指针停止,得到 i 的值3. 根据 得到 i e1 e2 判断大小,并且确定变化的节点,确定Array是 增加 | 删除 | 修改 | 移动- 这里 左侧对比 位创建 D E 节点- 基于指针的判断 i <= e1 && i <= e2- 调用 patch() 进行创建 渲染
实现左侧对比
/* renderer.ts */// Array -> Array : diff 算法的逻辑function patchKeyChildren(c1, c2, container, parentComponet) {/*** 1. 左侧对比* ( A B ) C* ( A B ) D E** 实现思路:* 1. 基于指针 i e1 e2* - 其中 i 指针为 0 , 指向两个数组的头部* - e1 为 老节点的 尾指针, 值为 e1 = n1.children.length - 1 Array1 的最后一个节点元素* - e2 为新节点的 尾指针, 值尾 e2 = n2.children.length - 1 Array2 的最后一个节点元素** 2. 基于左侧对比, n1.children [i] 于 n2.children [i] 比较* - 左侧元素相同, 把 i 指针向后一个位 移动 i++* - 当两个Array元素不同时,i指针停止,得到 i 的值** 3. 根据 得到 i e1 e2 判断大小,确定Array是 增加 | 删除 | 修改 | 移动* - 这里 左侧对比 位创建 D E 节点* - 调用 patch() 进行创建 渲染*/// 1. 创建 i e1 e2 指针索引let i = 0; // 指向两个数组的头部let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点元素let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点元素// 左侧对比// 2. 循环对比while(i <= e1 && i <= e2) { // 因为 i 不可能 大于 e1 e2 , 只能在 e1 e2 这个范围内遍历// 取出节点const n1 = c1[i]const n2 = c2[i]// 实现判断 n1 n2 是否一样的函数function isSomeVNodeType(n1, n2) {// 基于 key || type 判断 两个节点是否相同return n1.type === n2.type && n1.key === n2.key}// 判断连个节点if (isSomeVNodeType(n1, n2)) {// 如果相同, 调用 patch() 函数,进行更深层次的对比patch(n1, n2, container, parentComponent)}else {// 如果 n1 n2 不同// 退出循环, 然后 i++break}// 移动 i 的指针i++}// 查看 i 的指针, 根据 DEMO 左侧对比完 i 的值应该为 2console.log(i)}
在虚拟节点中初始化 key
/* vnode.ts */const vnode = {// 初始化 keykey: props && props.key,}
到此就完成了左侧的对比,主要的逻辑是算出指针 i ,算出不同节点时的指针;才根据指针进行 之后的 创建、删除等逻辑
6.2 右侧对比

- 右侧
- Array1: A B C
- Array2: D E B C
- 基于右侧找到形同的元素 B C , 得到乱序的元素 A D E , 然后基于乱序元素进行对比, 确定要改变的元素 A -> E D
happy patch
/* ArrayToArray.js */// 2.右侧对比// A ( B C )// D E (B C )const prevChildren = [// 多谢了一个 Keyh("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];const nextChildren = [h("p", { key: "D" }, "D"),h("p", { key: "E" }, "E"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];// 其他代码
抽离 isSomeVNodeType() 判断节点相同的方法,因为之后代码都需要用
/* renderer.ts*/function patchKeyedChildren(c1, c2, container, parentComponent) {let i = 0let e1 = c1.length - 1let e2 = c2.length - 1function isSomeVNodeType(n1, n2) {// 基于 key || 基于 VNode.type 判断两个节点的不同return n1.type === n2.type && n1.key === n2.key}}
实现右侧对比
实现思路
1. 基于指针 i e1 e2- 基于指针的判断 i <= e1 && i <= e2 , 因为 i 只能在这个范围2. 循环 n2 n2 对比- i 初始为 0, 因为是左侧对比,i 基本固定不动- 当 n1 n2 形同时, 移动 e1 e2 -> e1--, e2--- 当 n1 n2 不同时, 得到变化的节点,和 e1 e2 变化的值3. 根据 得到 i e1 e2 判断大小,确定Array是 增加 | 删除 | 修改 | 移动- 这里 右侧对比 为 删除 A ,创建 D E 节点
代码实现
/* renderer.ts */function patchKeyedChildren(c1, c2, container, parentComponent) {let i = 0 // 指向两个数组的头部let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点function isSomeVNodeType(n1, n2) {// 基于 key || 基于 VNode.type 判断两个节点的不同return n1.type === n2.type && n1.key === n2.key}// 其他代码// 2.右侧对比// A ( B C )// D E (B C )/*** 1. 基于指针 i e1 e2* - 基于指针的判断 i <= e1 && i <= e2 , 根左侧逻辑一样, i 只能在这个范围* 2. 循环 n2 n2 对比* - i 初始为 0, 因为是左侧对比,i 基本固定不动* - 当 n1 n2 相同时, 移动 e1 e2 -> e1--, e2--* - 当 n1 n2 不同时, 得到变化的节点,和 e1 e2 变化的值** 3. 根据 得到 i e1 e2 判断大小,确定Array是 增加 | 删除 | 修改 | 移动* - 这里 右侧对比 为 删除 A ,创建 D E 节点** 4. 基于当前 Array 最后的出 i = 0 && e1 = 0 e2 = 1 -> 确认变化的范围*/// 2.1 循环对比while (i <= e1 && i <= e2) {// 2.2 取出右侧的节点const n1 = c1[e1]const n2 = c2[e2]// 判断if (isSomeVNodeType(n1, n2)) {// 2.3. 如果相同,调用 patch() 递归进行对比patch(n1, n2, container, parentComponent)} else {// 如果不同时,退出循环break}// 2.4 移动 e1 e2e1--e2--}// 查看 i e1 e2 的值; 根据DEMO i = 0 | e1 = 0 | e2 = 1console.log(i, e1, e2)}
6.3 diff算法 - 创建节点
左侧一样,新的Array比老的Array长 - 创建新节点

组件测试
/*example/update/patchChildren/ArrayToArray.js */// 对比场景// 3 左侧一样, 新的比老的长 -> 创建节点const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];
实现创建节点的逻辑
/*** 对比场景** 1. 左侧一样, 新的比老的长* - Array1: A B* - Array2: A B C* - Array2: A B C D* - 基于左侧对比,得到变化的元素,创建 C , 把新创建的 C 添加到尾部** 实现对比-> 创建 C 节点* 1. 两个Array 进行对比, 因为是左侧一样, 新的比老的长,多出了 C 节点* 2. 基于指针 i e1 e2 , 确定 C节点的位置* 3. 创建 C 节点 添加到 Array2 尾部** - 最后指针为 i = 1 | e1 = 1 | e2 = 2* - 创建 C 节点,添加到 Array2 尾部* - 所以 当i > e1 && i <= e2 时创建节点**/
代码实现
/* renderer.ts */function patchKeyedChildren(c1, c2, container, parentComponent) {let i = 0 // 指向两个数组的头部let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点function isSomeVNodeType(n1, n2) {// 基于 key || 基于 VNode.type 判断两个节点的不同return n1.type === n2.type && n1.key === n2.key}// 其他代码/*** 3. 左侧一样, 新的比老的长, 进行创建节点* - Array1: A B* - Array2: A B C D* - 基于左侧对比,得到变化的元素,创建 C , 把新创建的 C 添加到尾部** 实现对比-> 创建 C 节点* 1. 两个Array 进行对比, 因为是左侧一样, 新的比老的长,多出了 C 节点* 2. 基于指针 i e1 e2 , 确定 C节点的位置* 3. 创建 C 节点 添加到 Array2 尾部** - 最后指针为 i = 1 | e1 = 1 | e2 = 2* - 创建 C 节点,添加到 Array2 尾部* - 所以 i > e1 && i <= e2 时创建节点**/if (i > e1) {if ( i <= e2) {// i > e1 && i <= e2 时创建节点 -> 因为确定 新Array 比老 Array 长// 调用 patch() 进行创建// 因为左侧对比到不同时,C 节点时, n1 是没有值的, n2 就是需要创建的新节点// 把创建的节点 C 添加到尾部patch(null, c2[i], container, parentComponent)}}}
效果图, 创建 C 节点 
右侧一样, 新的Array比老的长Array - 创建新节点

组件逻辑
// 4. 右侧一样, 新的比老的长const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),];const nextChildren = [h("p", { key: "C" }, "C"),h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),];
实现思路
2. 右侧一样, 新的比老的长 -- 创建新节点逻辑- Array1: A B- Array2: C A B- 基于右侧对比,得到变化的元素, 创建 C ,把新创建的 C 添加到头部实现对比-> 创建 C 节点1. 两个Array 进行对比,基于右侧对比, 因为是右侧一样, 新的比老的长,多出了 C 节点2. 基于指针 i e1 e2 , 确定 C节点的位置3. 创建 C 节点 添加到 Array2 头部- 右侧进行对比完,最后的指针为 i = 0 | e1 = -1 | e2 = 0- 所以在 i > e1 && i <= e2 时 创建新的节点- 创建 C 节点,添加到 Array2 头部 , 指定的位置- 实现添加到头部需要 : 锚点 -> 基于锚点 把创建的节点添加锚点的前一位置
代码实现
/*renderer.ts*//*** 4. 右侧一样, 新Array的比老Array的长 - 创建节点* - Array1: A B* - Array2: C A B* - 基于右侧对比,得到变化的元素, 创建 C ,把新创建的 C 添加到头部** 实现对比-> 创建 C 节点* 1. 两个Array 进行对比,基于右侧对比, 因为是右侧一样, 新的比老的长,多出了 C 节点* 2. 基于指针 i e1 e2 , 确定 C节点的位置* 3. 创建 C 节点 添加到 Array2 头部** - 右侧进行对比完,最后的指针为 i = 0 | e1 = -1 | e2 = 0* - 所以在 i > e1 && i <= e2 时 创建新的节点* - 创建 C 节点,添加到 Array2 头部 , 指定的位置* - 实现添加到头部需要 : 锚点 -> 基于锚点 把创建的节点添加锚点的前一位置*/// 因为 左侧和右侧一样, 判单创建节点,指针的的值范围都一样, 进行合并写if (i > e1) {if (i <= e2) {// i > e1 && i <= e2 时创建节点 -> 因为确定 新Array 比老 Array 长,所以需要创建节点// 调用 patch() 进行创建// 因为左侧对比到不同时,C 节点时, n1 是没有值的, n2 就是需要创建的新节点// 把创建的节点 C 添加到尾部// patch(null, c2[i], container, parentComponent)// 声明一个锚点, 锚点等于 A 节点的 el , 把创建的节点 添加打A节点前// 声明一个 位置 nextPosconst nextPos = i + 1 // 因为右侧对比,i 一直为0; i+1 就是就是获取锚点的位置// 因为这里的逻辑时 左侧对比 & 右侧对比 新节点比老节点长。 的逻辑都会执行这里// 所以需要设置一个锚点,根据锚点判断,是添加在 头部 还是 尾部const anchor = i + 1 > c2.length ? null : c2[nextPos].el// 分析: 判断 i + 1 > c2.length 新节点的元素个数, 如果大于那就是进行了左侧对比, 所以赋值锚点为 null , 表示添加到最后的尾部位置// 如果 i + 1 没有大于 新节点的元素个数, 说明进行的是右侧对比,赋值锚点为 c2[i+1].el, 新节点的第二个位置的元素 : A ,就是锚点,// 给 patch 函数 赋值锚点patch(null, c2[i], container, parentComponent, anchor)}}
renderer.ts patch() 新增 描点
/* renderer.ts */// 把锚点赋值为 null, 因为初始时候patch(null, vnode, container, null, null);function patch(n1, n2, container, parentComponent, anchor) {...}// 其他需要 patch() 的函数都需要加上 anchor 描点// 在mountElement 时, 添加到容器function mountElement(vnode, container, parentComponent, anchor) {// 4. 挂载 el 到 容器 container 上// container.appendChild(el)// 接口3 insert()// 添加到容器hostInsert(el, container, anchor);}
在 runtime-dom 中 insert() 增加 描点
// 添加 anchor 锚点function insert(child, parent, anchor) {// 基于 DOM 实现的// parent.append(el)// 添加节点到执行位置之前 beforeinsert()// 这里设置 默认锚点的位置为 null, 如果没有传值,默认为 null, 也就是默认添加到最后, 和append 一样parent.insertBefore(child, anchor || null)}
效果预览 
修复 bug
bug 出现的原因:Array1: A BArray2: D C A B这里出现 bug, 当新的 Array 创建多个节点时,C D , 会出现问题,它会添加到最后,而不是头部- 分析: 当进行右侧对比后: i = 0 | e1 = -1 | e2 = 1 , 然后进入到创建的逻辑。但是在设置 nextPos = i + 1, 指定到C节点,而此时C节点还没有创建, 所以 nextPos = null- 解决: 应该获取锚点是是基于 e2 + 1 -> 取到 A 节点这样创建节点时基于锚点 A,在A前面插入 D 节点 -> 在A前面插入 C 节点
在获取 新Array2 的头部节点 (描点)时,改为 e2 + 1 , 并且增加 循环创建多个节点的逻辑
/* renderer.ts */if (i > e1) {if (i <= e2) {const nextPos = e2 + 1 // 修改 bug : 改为 使用 e2 + 1 获取到 Array2 创建时 A 的描点const anchor = nextPos < l2 ? c2[nextPos].el : null // 改为 nextPos 判断左右的对比,// 因为创建的节点, 不仅仅是一个, 可能还有 C D// 循环一个 循环 , 进行创建while (i <= e2) {// 给 patch 函数 赋值锚点patch(null, c2[i], container, parentComponent, anchor)// 创建玩当前节点后,把 i 指针向后移动一位i++}}}
6.4 diff 算法 - 删除节点
左侧一样,老的Array的比新的Array长 - 删除老节点

测试组件
/* example/update/patchChildren/ArrayToArray.js */// 当老的Array 比 新的 Array长时 , -> 进行删除逻辑// 5. 基于左侧对比 -> Array1 比 Array2 长 执行删除 老节点逻辑const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),];
实现逻辑
执行删除的逻辑左侧对比 Array1 比 Array2 执行删除 老节点逻辑* 3. Array1 比 Array2 长* - Array1: A B C* - Array2: A B* - 基于左侧对比, 找到乱序元素 C , 执行删除 C** 实现:* 1. 两个Array 进行对比,基于左侧对比, 因为是左侧一样,老的比新的长,多出了 C 节点,所以需要删除 C 节点* 2. 在进行左侧对比时, 基于指针 i e1 e2 , 确定 C节点的位置* 3. 最后指针为 i = 2 | e1 = 2 | e2 = 1 , 确定多出了 C 节点* 4. 执行删除 C 节点** - 进行判断删除的逻辑* - i > e2 && i <= e1 时,执行删除逻辑 (主要是 i > e2 && i <= e1))* - 删除 C 节点
在patchKeyedChildren 逻辑中操作节点的逻辑 , 中增加判断
/* renderer.ts */if (i > e1) {if (i <= e2) {// 创建节点逻辑}} else if (i > e2) { // 增加判断逻辑// 执行删除的逻辑// 左侧对比 Array1 比 Array2 执行删除 老节点逻辑/*** 3. Array1 比 Array2 长* - Array1: A B C* - Array2: A B* - 基于左侧对比, 找到乱序元素 C , 执行删除 C** 实现:* 1. 两个Array 进行对比,基于左侧对比, 因为是左侧一样,老的比新的长,多出了 C 节点,所以需要删除 C 节点* 2. 在进行左侧对比时, 基于指针 i e1 e2 , 确定 C节点的位置* 3. 最后指针为 i = 2 | e1 = 2 | e2 = 1 , 确定多出了 C 节点* 4. 执行删除 C 节点** - 进行判断删除的逻辑* - i > e2 && i <= e1 时,执行删除逻辑 (主要是 i > e2 && i <= e1))* - 删除 C 节点*/// 循环删除while (i <= e1) {// 执行删除多出的节点 c1[i].el 使用 hostRemove 接口删除hostRemove(c1[i].el)// 移动 i 的指针i++}} else {// 乱序的部分, 中间的部分}
右侧一样,老的Array的比新的Array长 - 删除老节点

组件模拟
/* example/update/patchChildren/ArrayToArray.js */// 当老的Array 比 新的 Array长时 , -> 进行删除逻辑// 6. 基于右侧对比const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];const nextChildren = [h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),];
实现逻辑
4. Array1 比 Array2 长- Array1: A B C- Array2: B C- 基于左侧对比, 得到变化的元素 A , 执行删除 A- 基于右侧对比, 得到指针数据, i = 0 | e1 = 0 | e2 = -1- 发现于左侧对比,实现的判断一样 i <= e1 ; i > e2 , 执行删除逻辑- 删除 首部节点 A
6.5 diff 算法 - 中间对比

// 两个Array 中间 进行对比双端对比中, 最复杂的中间元素的对比, 也是 双端对比的最核心部分中间元素乱序的几种情况Array1: A B C Y E F GArray2: A B E D C F G中间乱序: C Y E -> E D C1. 创建新的 D -> 老的节点中不存在, 新的节点中存在2. 删除老的节点中 Y -> 在老节点的里面存在, 新节点里面不存在3. 移动 C E -> 节点存在于新的和老的节点里, 但是位置发生改变了
6.5.1 中间对比 - 删除老节点
组件测试代码
/* example/update/patchChildren/ArrayToArray.js */const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C", id: "c-prev" }, "C"),h("p", { key: "D" }, "D"),h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "E" }, "E"), // 增加了E节点 & 删除 D 节点h("p", { key: "C", id: "c-next" }, "C"), // props不同h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];
实现中间乱序节点, 删除的逻辑
// 1. 删除Array2 中的节点 D -> 老节点中存在, 新节点中不存在 (移动|创建先不实现)Array1: A B C D F GArray2: A B E C F G在Array1 和 Array2 已经进行过双端对比,得到中间节点的不同, 乱序的节点- C D- E C- 删除 D 并且创建 E实现思路:找到中间乱序的节点, 进行遍历一遍 -> 在新Array2中创建映射表,遍历老Array1, 根据映射表,删除映射表的节点 -> D 节点- 还有一种遍历方法,也就是通过 key 查找节点。 -> 新节点key 是否在老节点 key 里面1. 经过左侧与右侧的对比,得到中间节点的不同, 乱序的节点- 此时的指针为 i = 2 | e1 = 3 | e2 = 32. 创建 基于新Array2映射表, key -> value, key 就是节点的key, value 为节点的索引值3. 老节点遍历,根据key 查找对应的映射表, 如果没有对应上,就删除老Array1中 节点-> D 节点- 如果查找到对应的 映射表,调用 patch() 进行对比,可以 props | children不同需要进行修改
具体代码实现
/* renderer.ts*/{// 其他代码} else {// 乱序的部分, 中间的部分// 1. 删除Array2 中的节点 D -> 老节点中存在, 新节点中不存在 (移动|创建先不实现)/*** Array1: A B C D F G* Array2: A B E C F G**** 在Array1 和 Array2 已经进行过双端对比,得到中间节点的不同, 乱序的节点* - C D* - E C* - 删除 D 并且创建 E** 实现思路:找到中间乱序的节点, 进行遍历一遍 -> 在新Array2中创建映射表,遍历老Array1, 根据映射表,删除映射表的节点 -> D 节点* - 还有一种遍历方法,也就是通过 key 查找节点。 -> 新节点key 是否在老节点 key 里面** 1. 经过左侧与右侧的对比,得到中间节点的不同, 乱序的节点* - 此时的指针为 i = 2 | e1 = 3 | e2 = 3* 2. 创建 基于新Array2映射表, key -> value, key 就是节点的key, value 为节点的索引值* 3. 老节点遍历,根据key 查找对应的映射表, 如果没有对应上,就删除老Array1中 节点-> D 节点* - 如果查找到对应的 映射表,调用 patch() 进行对比,可以 props | children不同需要进行修改*/// 此时的指针为 i = 2 | e1 = 3 | e2 = 3// 1. 需要遍历 Array1 和 Array2 中间乱序的部分// 创建指针let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置// 2. 基于 新Array2 创建映射表, 存储Array2 中间乱序的节点部分const keyToNewIndexMap = new Map<string, number>()for (let i = s2; i <= e2; i++) {// 获取到当前节点const nextChild = c2[i]// 添加到映射表中, key -> value , key 就是节点的key, value 为节点的索引值keyToNewIndexMap.set(nextChild.key, i)}// 定义一个遍历,保存 Array1 节点 在 Array2 中的位置let newIndex// 3. 遍历 Array1 中间乱序的部分for (let i = s1; i <= e1; i++) {// 获取到 Array1 当前的节点const prevChild = c1[i]// 判断 Array1 当前节点 的属性是否具有key// 当前节点 没有key, 那么key就有两种 情况 null | undefinedif (prevChild.key !== null) {// 当有key 时,才会去判断是否在映射表中, 因为映射表基于 Array2 乱序节点 key 创建的// 根据在老Array 真正遍历的节点 key, 查找对应的映射表// 如果能获取到值 -> 新Array2中 一定存在该节点// 如果没有 -> 说明该节点在新Array2中不存在,需要删除newIndex = keyToNewIndexMap.get(prevChild.key) // 返回索引值, 新节点所在位置// 当 key 没有时 newIndex 为 undefined} else {// 如果没有 当前节点 没有设置 key// 遍历 新Array2 中的节点,对比 Array1 中的节点, 判断是否存在// 如果不相同,删除老节点for (let j = s2; i <= e2; i++) {// 判断 新老节点是否相同if (isSomeVNodeType(prevChild, c2[j])) {// 进行 newIndex 赋值 -> 新节点所在的位置newIndex = j// 查找到节点, 就没有必要遍历了break}}}// 4. 基于 newIndex索引,判断节点的状态// 判断 newIndex 是否存在if (newIndex === undefined) {// 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除// 删除老节点 ,也就是当前节点hostRemove(prevChild.el)} else {// 如果 newIndex 具有值 -> 新节点所在的位置// 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性patch(prevChild, c2[newIndex], container, parentComponent, null)}}}
优化删除的逻辑
/* example/update/patchChildren/ArrayToArray.js */const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C", id: "c-prev" }, "C"),// 新增了 E 节点h("p", { key: "E" }, "E"),h("p", { key: "D" }, "D"),h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "E" }, "E"), // 增加了E节点 & 删除 D 节点h("p", { key: "C", id: "c-next" }, "C"), // props不同h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];
优化思路
// 2. 优化删除逻辑* Array1: A B C E D F G* Array2: A B E C F G** - i = 2 | e1 = 4 | e2 = 3* 优化的点: 当遍历Array1, 对应映射表时,如果映射表中的项已经映射完了,就不再进行循环映射了,直接删除Array1 之后的节点* 实现:* 1. 记录新Array 变化的数量, 当每次映射时候,定义一个变量记录下来* 2. 判断记录的数量 超过 新节点的数量时,Array1 后面的所有节点都进行移除*
代码实现
/* renderer.ts*/{// 其他代码} else {// 乱序的部分, 中间的部分let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置// 定义一个变量 , 记录新Array2 中间乱序节点的数量const toBePatched = e2 - s2 + 1 // 3 - 2 + 1 -> 新Array2 乱序节点的数量// 定义一个变量 记录新Array1 已经映射过的节点数量 当每次映射时候,定义一个变量记录下来let patched = 0 // Array1 已经映射过节点的数量// 2. 基于 新Array2 创建映射表, 存储Array2 中间乱序的节点部分const keyToNewIndexMap = new Map<string, number>()for (let i = s2; i <= e2; i++) {// 获取到当前节点const nextChild = c2[i]// 添加到映射表中, key -> value , key 就是节点的key, value 为节点的索引值keyToNewIndexMap.set(nextChild.key, i)}// 定义一个遍历,保存 Array1 节点 在 Array2 中的位置let newIndex// 3. 遍历 Array1 中间乱序的部分for (let i = s1; i <= e1; i++) {// 获取到 Array1 当前的节点const prevChild = c1[i]// 这里每次都需要去检查 patched 是否超过 toBePatched,如果超过,就不再进行循环了// patched 映射的次数 >= toBePatched 新节点的数量, 如果超过了,直接删除老节点if (patched >= toBePatched) {// 直接删除了,没必要执行下面的逻辑hostRemove(prevChild.el)continue}// 判断 Array1 当前节点 的属性是否具有key// 当前节点 没有key, 那么key就有两种 情况 null | undefinedif (prevChild.key !== null) {// 当有key 时,才会去判断是否在映射表中, 因为映射表基于 Array2 乱序节点 key 创建的// 根据在老Array 真正遍历的节点 key, 查找对应的映射表// 如果能获取到值 -> 新Array2中 一定存在该节点// 如果没有 -> 说明该节点在新Array2中不存在,需要删除newIndex = keyToNewIndexMap.get(prevChild.key) // 返回索引值, 新节点所在位置// 当 key 没有时 newIndex 为 undefined} else {// 如果没有 当前节点 没有设置 key// 遍历 新Array2 中的节点,对比 Array1 中的节点, 判断是否存在// 如果不相同,删除老节点for (let j = s2; i <= e2; i++) {// 判断 新老节点是否相同if (isSomeVNodeType(prevChild, c2[j])) {// 进行 newIndex 赋值 -> 新节点所在的位置newIndex = j// 查找到节点, 就没有必要遍历了break}}}// 4. 基于 newIndex索引,判断节点的状态// 判断 newIndex 是否存在if (newIndex === undefined) {// 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除// 删除老节点 ,也就是当前节点hostRemove(prevChild.el)} else {// 如果 newIndex 具有值 -> 新节点所在的位置// 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性patch(prevChild, c2[newIndex], container, parentComponent, null)// 这里表示已经映射完一个节点了,需要在这里赋值 patched + 1patched++}}}
改完代码, 根据DEMO,当Array1 遍历到 D 节点时, Array2创建的映射表, 已经映射完了,所以 D 节点直接删除,不会执行深层的逻辑
6.5.2 中间对比 - 节点的移动
测试组件
// 中间节点 - 移动 - 节点存在于新的和老的里面,但是位置变了// a b (c d e) f g// a b (e c d) f g// 最长递增子序列 [1, 2] -> c dconst prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),h("p", { key: "D" }, "D"),h("p", { key: "E" }, "E"),h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "E" }, "E"), // E 节点移动h("p", { key: "C" }, "C"),h("p", { key: "D" }, "D"),h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];// 3. 中间节点移动节点的逻辑/*** 1. 确定中间节点的不同,并且建立中间节点, 新Array 与 老Array 的映射关系* 2. 初始化映射表* 3. 赋值映射表* 4. 求最长递增子序列, 也就是新节点中不变的节点,一共有多少个* 5. 进行两个节点的对比*/
代码实现
/* renderer.ts */else {// 1. 需要遍历 Array1 和 Array2 中间乱序的部分// 创建指针let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置// 定义一个变量 , 记录新Array2 中间乱序节点的数量const toBePatched = e2 - s2 + 1 // 3 - 2 + 1 -> 新Array2 乱序节点的数量// 定义一个变量 记录新Array1 已经映射过的节点数量 当每次映射时候,定义一个变量记录下来let patched = 0 // Array1 已经映射过节点的数量let moved = falselet maxNewIndexSofar = 0// 3. 中间节点移动节点的逻辑/*** 1. 确定中间节点的不同,并且建立中间节点, 新Array 与 老Array 的映射关系* 2. 初始化映射表* 3. 赋值映射表* 4. 求最长递增子序列, 也就是新节点中不变的节点,一共有多少个* 5. 进行两个节点的对比*/// 3.1. 建立映射表 新Array 与 老Array 的映射关系// 给定一个定长的数组, 因为性能更好,数组范围是,Array2 中间节点变化的数量const newIndexToOldIndexMap = new Array(toBePatched)// 3.2. 初始化 映射表, 赋值为 i, i -> Array1 & Array2 的开始遍历位置// 表示没有建立映射关系// 循环Array2, 初始化这个映射表for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0// 其他代码// 判断 newIndex 是否存在if (newIndex === undefined) {// 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除// 删除老节点 ,也就是当前节点hostRemove(prevChild.el)} else {// 3.3 判断if (newIndex >= maxNewIndexSofar) {maxNewIndexSofar = newIndex} else {moved = true}// 这里赋值这个映射表newIndexToOldIndexMap[newIndex - s2] = i + 1// 如果 newIndex 具有值 -> 新节点所在的位置// 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性patch(prevChild, c2[newIndex], container, parentComponent, null)// 这里表示已经映射完一个节点了,需要在这里赋值 patched + 1patched++}// 4. 获取最长递增子序列const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []// 5. 进行节点的对比, 一个是新节点,一个是最长递增子序列// 定义索引指针, 用于指向最长递增子序列let j = increasingNewIndexSequence.length - 1// 遍历 Array2for (let i = toBePatched - 1; i >= 0; i--) {// 调用 insert() 方法 -> 基于锚点const nextIndex = i + s2const nextChild = c2[nextIndex]// 锚点const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null// 创建节点逻辑if (newIndexToOldIndexMap[i] === 0) {patch(null, nextChild, container, parentComponent, anchor);} else if (moved) { // 判断移动if (j < 0 || i !== increasingNewIndexSequence[j]) {// 进行移动位置// console.log("移动位置")hostInsert(nextChild.el, container, anchor)} else {// 不需要移动j--}}}}
求最长递增子序列
/* renderer.ts */// 根据映射表,求最长递增子序列function getSequence(arr) {const p = arr.slice();const result = [0];let i, j, u, v, c;const len = arr.length;for (i = 0; i < len; i++) {const arrI = arr[i];if (arrI !== 0) {j = result[result.length - 1];if (arr[j] < arrI) {p[i] = j;result.push(i);continue;}u = 0;v = result.length - 1;while (u < v) {c = (u + v) >> 1;if (arr[result[c]] < arrI) {u = c + 1;} else {v = c;}}if (arrI < arr[result[u]]) {if (u > 0) {p[i] = result[u - 1];}result[u] = i;}}}u = result.length;v = result[u - 1];while (u-- > 0) {result[u] = v;v = p[v];}return result;}
6.5.3 中间对比 - 创建节点
// 3. 创建新的节点// a,b,(c,e),f,g// a,b,(e,c,d),f,g// d 节点在老的节点中不存在,新的里面存在,所以需要创建const prevChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "C" }, "C"),h("p", { key: "E" }, "E"),h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];const nextChildren = [h("p", { key: "A" }, "A"),h("p", { key: "B" }, "B"),h("p", { key: "E" }, "E"), // 移动 E 节点h("p", { key: "C" }, "C"),h("p", { key: "D" }, "D"), // 创建 D 节点h("p", { key: "F" }, "F"),h("p", { key: "G" }, "G"),];
代码实现
/* renderer.ts */// 4. 获取最长递增子序列const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []// 5. 进行节点的对比, 一个是新节点,一个是最长递增子序列// 定义索引指针, 用于指向最长递增子序列let j = increasingNewIndexSequence.length - 1// 遍历 Array2for (let i = toBePatched - 1; i >= 0; i--) {// 调用 insert() 方法 -> 基于锚点const nextIndex = i + s2const nextChild = c2[nextIndex]// 锚点const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null// 创建节点逻辑if (newIndexToOldIndexMap[i] === 0) {patch(null, nextChild, container, parentComponent, anchor);} else if (moved) { // 判断移动if (j < 0 || i !== increasingNewIndexSequence[j]) {// 进行移动位置// console.log("移动位置")hostInsert(nextChild.el, container, anchor)} else {// 不需要移动j--}}}

