这里实现 Element更新中 childrenArray -> Array 的逻辑

Array1 -> Array2 之前的对比,不单单考虑删除老的 Array, 创建新的 Array而已,需要考虑更过的场景来适配 Element 的更新 , 因为在前端领域中,往往页面中数据发生的改变,都是Element 节点中间的一小部分。
例如

  • Element 节点中, 只有一个数据发生变化 ,不可能遍历全部Array来查找改变的数据,这样开销太大,性能也不行

所以Vue实现的 diff 算法,用于适配 Element更新场景, 找出Element 中发生的改变, 进行Element 的更新

Array -> Array的几种情况

使用双端对比算法 Array -> Array
双端对比算法的核心: 就是筛选出 两个 Array 中间的 乱序元素

6.1 左侧对比

image.png
1. 左侧
- Array1: A B C
- Array2: A B D E
- 找出左侧相同的元素, 即 A B,得到乱序的元素 D E ,然后基于乱序元素进行对比, 确定要改变的元素 C -> D E

happy patch

  1. /* ArrayToArray.js*/
  2. // 1. 左侧对比
  3. // ( A B ) C
  4. // ( A B ) D E
  5. const prevChildren = [
  6. // 多谢了一个 Key
  7. h("p", { key: "A" }, "A"),
  8. h("p", { key: "B" }, "B"),
  9. h("p", { key: "C" }, "C"),
  10. ];
  11. const nextChildren = [
  12. h("p", { key: "A" }, "A"),
  13. h("p", { key: "B" }, "B"),
  14. h("p", { key: "D" }, "D"),
  15. h("p", { key: "E" }, "E"),
  16. ];
  17. // 导出手动改变的的组件
  18. export default {
  19. name: "ArrayToArray",
  20. setup() {
  21. // 定义响应式变量
  22. const isChange = ref(false)
  23. window.isChange = isChange
  24. return {
  25. isChange
  26. }
  27. },
  28. render() {
  29. const self = this
  30. // 实现根据 isChange 判断显示的 节点
  31. return self.isChange === true ? h("div", {}, nextChildren) : h("div", {}, prevChildren)
  32. }
  33. };

image.png
接上节 Element 更新的变化, Array -> Array 的逻辑

  1. /* renderer.ts */
  2. /* 其他代码 */
  3. if (shapeFlag & ShapeFlags.TEXT_CHILDREN){
  4. // 其他代码
  5. } else { // 更新后的节点是一个 Array
  6. if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
  7. // Text -> Array
  8. // 1. 把Text清空
  9. }else {
  10. // Array -> Array
  11. // 老Array -> 新Array
  12. // 使用 patchKeyChildren 函数实现
  13. // 传入 老的 Array 和 新的 Array, container, parentComponet 之后patch()渲染函数会需要
  14. patchKeyChildren(c1, c2, container, parentComponet)
  15. }
  16. }

实现左侧对比的逻辑

实现对左侧对比的思路

  1. 实现思路:
  2. 1. 基于指针 i e1 e2
  3. - 其中 i 指针为 0 指向两个数组的头部
  4. - e1 老节点的 尾指针, 值为 e1 = n1.children.length - 1 Array1 的元素个数
  5. - e2 为新节点的 尾指针, 值尾 e2 = n2.children.length - 1 Array2 的元素个数
  6. 2. 基于左侧对比, n1.children [i] n2.children [i] 比较
  7. - 左侧元素相同, i 指针向后一个位 移动 i++
  8. - 当两个Array元素不同时,i 指针停止,得到 i 的值
  9. 3. 根据 得到 i e1 e2 判断大小,并且确定变化的节点,确定Array 增加 | 删除 | 修改 | 移动
  10. - 这里 左侧对比 位创建 D E 节点
  11. - 基于指针的判断 i <= e1 && i <= e2
  12. - 调用 patch() 进行创建 渲染

实现左侧对比

  1. /* renderer.ts */
  2. // Array -> Array : diff 算法的逻辑
  3. function patchKeyChildren(c1, c2, container, parentComponet) {
  4. /**
  5. * 1. 左侧对比
  6. * ( A B ) C
  7. * ( A B ) D E
  8. *
  9. * 实现思路:
  10. * 1. 基于指针 i e1 e2
  11. * - 其中 i 指针为 0 , 指向两个数组的头部
  12. * - e1 为 老节点的 尾指针, 值为 e1 = n1.children.length - 1 Array1 的最后一个节点元素
  13. * - e2 为新节点的 尾指针, 值尾 e2 = n2.children.length - 1 Array2 的最后一个节点元素
  14. *
  15. * 2. 基于左侧对比, n1.children [i] 于 n2.children [i] 比较
  16. * - 左侧元素相同, 把 i 指针向后一个位 移动 i++
  17. * - 当两个Array元素不同时,i指针停止,得到 i 的值
  18. *
  19. * 3. 根据 得到 i e1 e2 判断大小,确定Array是 增加 | 删除 | 修改 | 移动
  20. * - 这里 左侧对比 位创建 D E 节点
  21. * - 调用 patch() 进行创建 渲染
  22. */
  23. // 1. 创建 i e1 e2 指针索引
  24. let i = 0; // 指向两个数组的头部
  25. let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点元素
  26. let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点元素
  27. // 左侧对比
  28. // 2. 循环对比
  29. while(i <= e1 && i <= e2) { // 因为 i 不可能 大于 e1 e2 , 只能在 e1 e2 这个范围内遍历
  30. // 取出节点
  31. const n1 = c1[i]
  32. const n2 = c2[i]
  33. // 实现判断 n1 n2 是否一样的函数
  34. function isSomeVNodeType(n1, n2) {
  35. // 基于 key || type 判断 两个节点是否相同
  36. return n1.type === n2.type && n1.key === n2.key
  37. }
  38. // 判断连个节点
  39. if (isSomeVNodeType(n1, n2)) {
  40. // 如果相同, 调用 patch() 函数,进行更深层次的对比
  41. patch(n1, n2, container, parentComponent)
  42. }else {
  43. // 如果 n1 n2 不同
  44. // 退出循环, 然后 i++
  45. break
  46. }
  47. // 移动 i 的指针
  48. i++
  49. }
  50. // 查看 i 的指针, 根据 DEMO 左侧对比完 i 的值应该为 2
  51. console.log(i)
  52. }

在虚拟节点中初始化 key

  1. /* vnode.ts */
  2. const vnode = {
  3. // 初始化 key
  4. key: props && props.key,
  5. }

到此就完成了左侧的对比,主要的逻辑是算出指针 i ,算出不同节点时的指针;才根据指针进行 之后的 创建、删除等逻辑

6.2 右侧对比

image.png

  1. 右侧
    - Array1: A B C
    - Array2: D E B C
    - 基于右侧找到形同的元素 B C , 得到乱序的元素 A D E , 然后基于乱序元素进行对比, 确定要改变的元素 A -> E D

happy patch

  1. /* ArrayToArray.js */
  2. // 2.右侧对比
  3. // A ( B C )
  4. // D E (B C )
  5. const prevChildren = [
  6. // 多谢了一个 Key
  7. h("p", { key: "A" }, "A"),
  8. h("p", { key: "B" }, "B"),
  9. h("p", { key: "C" }, "C"),
  10. ];
  11. const nextChildren = [
  12. h("p", { key: "D" }, "D"),
  13. h("p", { key: "E" }, "E"),
  14. h("p", { key: "B" }, "B"),
  15. h("p", { key: "C" }, "C"),
  16. ];
  17. // 其他代码

抽离 isSomeVNodeType() 判断节点相同的方法,因为之后代码都需要用

  1. /* renderer.ts*/
  2. function patchKeyedChildren(c1, c2, container, parentComponent) {
  3. let i = 0
  4. let e1 = c1.length - 1
  5. let e2 = c2.length - 1
  6. function isSomeVNodeType(n1, n2) {
  7. // 基于 key || 基于 VNode.type 判断两个节点的不同
  8. return n1.type === n2.type && n1.key === n2.key
  9. }
  10. }

实现右侧对比

实现思路

  1. 1. 基于指针 i e1 e2
  2. - 基于指针的判断 i <= e1 && i <= e2 , 因为 i 只能在这个范围
  3. 2. 循环 n2 n2 对比
  4. - i 初始为 0, 因为是左侧对比,i 基本固定不动
  5. - n1 n2 形同时, 移动 e1 e2 -> e1--, e2--
  6. - n1 n2 不同时, 得到变化的节点,和 e1 e2 变化的值
  7. 3. 根据 得到 i e1 e2 判断大小,确定Array 增加 | 删除 | 修改 | 移动
  8. - 这里 右侧对比 删除 A ,创建 D E 节点

代码实现

  1. /* renderer.ts */
  2. function patchKeyedChildren(c1, c2, container, parentComponent) {
  3. let i = 0 // 指向两个数组的头部
  4. let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点
  5. let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点
  6. function isSomeVNodeType(n1, n2) {
  7. // 基于 key || 基于 VNode.type 判断两个节点的不同
  8. return n1.type === n2.type && n1.key === n2.key
  9. }
  10. // 其他代码
  11. // 2.右侧对比
  12. // A ( B C )
  13. // D E (B C )
  14. /**
  15. * 1. 基于指针 i e1 e2
  16. * - 基于指针的判断 i <= e1 && i <= e2 , 根左侧逻辑一样, i 只能在这个范围
  17. * 2. 循环 n2 n2 对比
  18. * - i 初始为 0, 因为是左侧对比,i 基本固定不动
  19. * - 当 n1 n2 相同时, 移动 e1 e2 -> e1--, e2--
  20. * - 当 n1 n2 不同时, 得到变化的节点,和 e1 e2 变化的值
  21. *
  22. * 3. 根据 得到 i e1 e2 判断大小,确定Array是 增加 | 删除 | 修改 | 移动
  23. * - 这里 右侧对比 为 删除 A ,创建 D E 节点
  24. *
  25. * 4. 基于当前 Array 最后的出 i = 0 && e1 = 0 e2 = 1 -> 确认变化的范围
  26. */
  27. // 2.1 循环对比
  28. while (i <= e1 && i <= e2) {
  29. // 2.2 取出右侧的节点
  30. const n1 = c1[e1]
  31. const n2 = c2[e2]
  32. // 判断
  33. if (isSomeVNodeType(n1, n2)) {
  34. // 2.3. 如果相同,调用 patch() 递归进行对比
  35. patch(n1, n2, container, parentComponent)
  36. } else {
  37. // 如果不同时,退出循环
  38. break
  39. }
  40. // 2.4 移动 e1 e2
  41. e1--
  42. e2--
  43. }
  44. // 查看 i e1 e2 的值; 根据DEMO i = 0 | e1 = 0 | e2 = 1
  45. console.log(i, e1, e2)
  46. }

6.3 diff算法 - 创建节点

左侧一样,新的Array比老的Array长 - 创建新节点

image.png
组件测试

  1. /*example/update/patchChildren/ArrayToArray.js */
  2. // 对比场景
  3. // 3 左侧一样, 新的比老的长 -> 创建节点
  4. const prevChildren = [
  5. h("p", { key: "A" }, "A"),
  6. h("p", { key: "B" }, "B"),
  7. ];
  8. const nextChildren = [
  9. h("p", { key: "A" }, "A"),
  10. h("p", { key: "B" }, "B"),
  11. h("p", { key: "C" }, "C"),
  12. ];

实现创建节点的逻辑

  1. /**
  2. * 对比场景
  3. *
  4. * 1. 左侧一样, 新的比老的长
  5. * - Array1: A B
  6. * - Array2: A B C
  7. * - Array2: A B C D
  8. * - 基于左侧对比,得到变化的元素,创建 C , 把新创建的 C 添加到尾部
  9. *
  10. * 实现对比-> 创建 C 节点
  11. * 1. 两个Array 进行对比, 因为是左侧一样, 新的比老的长,多出了 C 节点
  12. * 2. 基于指针 i e1 e2 , 确定 C节点的位置
  13. * 3. 创建 C 节点 添加到 Array2 尾部
  14. *
  15. * - 最后指针为 i = 1 | e1 = 1 | e2 = 2
  16. * - 创建 C 节点,添加到 Array2 尾部
  17. * - 所以 当i > e1 && i <= e2 时创建节点
  18. *
  19. */

代码实现

  1. /* renderer.ts */
  2. function patchKeyedChildren(c1, c2, container, parentComponent) {
  3. let i = 0 // 指向两个数组的头部
  4. let e1 = c1.length - 1 // 老节点 Array1 的最后一个节点
  5. let e2 = c2.length - 1 // 新节点 Array2 的最后一个节点
  6. function isSomeVNodeType(n1, n2) {
  7. // 基于 key || 基于 VNode.type 判断两个节点的不同
  8. return n1.type === n2.type && n1.key === n2.key
  9. }
  10. // 其他代码
  11. /**
  12. * 3. 左侧一样, 新的比老的长, 进行创建节点
  13. * - Array1: A B
  14. * - Array2: A B C D
  15. * - 基于左侧对比,得到变化的元素,创建 C , 把新创建的 C 添加到尾部
  16. *
  17. * 实现对比-> 创建 C 节点
  18. * 1. 两个Array 进行对比, 因为是左侧一样, 新的比老的长,多出了 C 节点
  19. * 2. 基于指针 i e1 e2 , 确定 C节点的位置
  20. * 3. 创建 C 节点 添加到 Array2 尾部
  21. *
  22. * - 最后指针为 i = 1 | e1 = 1 | e2 = 2
  23. * - 创建 C 节点,添加到 Array2 尾部
  24. * - 所以 i > e1 && i <= e2 时创建节点
  25. *
  26. */
  27. if (i > e1) {
  28. if ( i <= e2) {
  29. // i > e1 && i <= e2 时创建节点 -> 因为确定 新Array 比老 Array 长
  30. // 调用 patch() 进行创建
  31. // 因为左侧对比到不同时,C 节点时, n1 是没有值的, n2 就是需要创建的新节点
  32. // 把创建的节点 C 添加到尾部
  33. patch(null, c2[i], container, parentComponent)
  34. }
  35. }
  36. }

效果图, 创建 C 节点
image.png

右侧一样, 新的Array比老的长Array - 创建新节点

image.png
组件逻辑

  1. // 4. 右侧一样, 新的比老的长
  2. const prevChildren = [
  3. h("p", { key: "A" }, "A"),
  4. h("p", { key: "B" }, "B"),
  5. ];
  6. const nextChildren = [
  7. h("p", { key: "C" }, "C"),
  8. h("p", { key: "A" }, "A"),
  9. h("p", { key: "B" }, "B"),
  10. ];

实现思路

  1. 2. 右侧一样, 新的比老的长 -- 创建新节点逻辑
  2. - Array1: A B
  3. - Array2: C A B
  4. - 基于右侧对比,得到变化的元素, 创建 C ,把新创建的 C 添加到头部
  5. 实现对比-> 创建 C 节点
  6. 1. 两个Array 进行对比,基于右侧对比, 因为是右侧一样, 新的比老的长,多出了 C 节点
  7. 2. 基于指针 i e1 e2 , 确定 C节点的位置
  8. 3. 创建 C 节点 添加到 Array2 头部
  9. - 右侧进行对比完,最后的指针为 i = 0 | e1 = -1 | e2 = 0
  10. - 所以在 i > e1 && i <= e2 创建新的节点
  11. - 创建 C 节点,添加到 Array2 头部 指定的位置
  12. - 实现添加到头部需要 锚点 -> 基于锚点 把创建的节点添加锚点的前一位置

代码实现

  1. /*renderer.ts*/
  2. /**
  3. * 4. 右侧一样, 新Array的比老Array的长 - 创建节点
  4. * - Array1: A B
  5. * - Array2: C A B
  6. * - 基于右侧对比,得到变化的元素, 创建 C ,把新创建的 C 添加到头部
  7. *
  8. * 实现对比-> 创建 C 节点
  9. * 1. 两个Array 进行对比,基于右侧对比, 因为是右侧一样, 新的比老的长,多出了 C 节点
  10. * 2. 基于指针 i e1 e2 , 确定 C节点的位置
  11. * 3. 创建 C 节点 添加到 Array2 头部
  12. *
  13. * - 右侧进行对比完,最后的指针为 i = 0 | e1 = -1 | e2 = 0
  14. * - 所以在 i > e1 && i <= e2 时 创建新的节点
  15. * - 创建 C 节点,添加到 Array2 头部 , 指定的位置
  16. * - 实现添加到头部需要 : 锚点 -> 基于锚点 把创建的节点添加锚点的前一位置
  17. */
  18. // 因为 左侧和右侧一样, 判单创建节点,指针的的值范围都一样, 进行合并写
  19. if (i > e1) {
  20. if (i <= e2) {
  21. // i > e1 && i <= e2 时创建节点 -> 因为确定 新Array 比老 Array 长,所以需要创建节点
  22. // 调用 patch() 进行创建
  23. // 因为左侧对比到不同时,C 节点时, n1 是没有值的, n2 就是需要创建的新节点
  24. // 把创建的节点 C 添加到尾部
  25. // patch(null, c2[i], container, parentComponent)
  26. // 声明一个锚点, 锚点等于 A 节点的 el , 把创建的节点 添加打A节点前
  27. // 声明一个 位置 nextPos
  28. const nextPos = i + 1 // 因为右侧对比,i 一直为0; i+1 就是就是获取锚点的位置
  29. // 因为这里的逻辑时 左侧对比 & 右侧对比 新节点比老节点长。 的逻辑都会执行这里
  30. // 所以需要设置一个锚点,根据锚点判断,是添加在 头部 还是 尾部
  31. const anchor = i + 1 > c2.length ? null : c2[nextPos].el
  32. // 分析: 判断 i + 1 > c2.length 新节点的元素个数, 如果大于那就是进行了左侧对比, 所以赋值锚点为 null , 表示添加到最后的尾部位置
  33. // 如果 i + 1 没有大于 新节点的元素个数, 说明进行的是右侧对比,赋值锚点为 c2[i+1].el, 新节点的第二个位置的元素 : A ,就是锚点,
  34. // 给 patch 函数 赋值锚点
  35. patch(null, c2[i], container, parentComponent, anchor)
  36. }
  37. }

renderer.ts patch() 新增 描点

  1. /* renderer.ts */
  2. // 把锚点赋值为 null, 因为初始时候
  3. patch(null, vnode, container, null, null);
  4. function patch(n1, n2, container, parentComponent, anchor) {
  5. ...
  6. }
  7. // 其他需要 patch() 的函数都需要加上 anchor 描点
  8. // 在mountElement 时, 添加到容器
  9. function mountElement(vnode, container, parentComponent, anchor) {
  10. // 4. 挂载 el 到 容器 container 上
  11. // container.appendChild(el)
  12. // 接口3 insert()
  13. // 添加到容器
  14. hostInsert(el, container, anchor);
  15. }

runtime-dominsert() 增加 描点

  1. // 添加 anchor 锚点
  2. function insert(child, parent, anchor) {
  3. // 基于 DOM 实现的
  4. // parent.append(el)
  5. // 添加节点到执行位置之前 beforeinsert()
  6. // 这里设置 默认锚点的位置为 null, 如果没有传值,默认为 null, 也就是默认添加到最后, 和append 一样
  7. parent.insertBefore(child, anchor || null)
  8. }

效果预览
image.png

修复 bug

  1. bug 出现的原因:
  2. Array1: A B
  3. Array2: D C A B
  4. 这里出现 bug, 当新的 Array 创建多个节点时,C D , 会出现问题,它会添加到最后,而不是头部
  5. - 分析: 当进行右侧对比后: i = 0 | e1 = -1 | e2 = 1 , 然后进入到创建的逻辑。
  6. 但是在设置 nextPos = i + 1 指定到C节点,而此时C节点还没有创建, 所以 nextPos = null
  7. - 解决: 应该获取锚点是是基于 e2 + 1 -> 取到 A 节点
  8. 这样创建节点时基于锚点 A,在A前面插入 D 节点 -> A前面插入 C 节点

在获取 新Array2 的头部节点 (描点)时,改为 e2 + 1 , 并且增加 循环创建多个节点的逻辑

  1. /* renderer.ts */
  2. if (i > e1) {
  3. if (i <= e2) {
  4. const nextPos = e2 + 1 // 修改 bug : 改为 使用 e2 + 1 获取到 Array2 创建时 A 的描点
  5. const anchor = nextPos < l2 ? c2[nextPos].el : null // 改为 nextPos 判断左右的对比,
  6. // 因为创建的节点, 不仅仅是一个, 可能还有 C D
  7. // 循环一个 循环 , 进行创建
  8. while (i <= e2) {
  9. // 给 patch 函数 赋值锚点
  10. patch(null, c2[i], container, parentComponent, anchor)
  11. // 创建玩当前节点后,把 i 指针向后移动一位
  12. i++
  13. }
  14. }
  15. }

image.png

6.4 diff 算法 - 删除节点

左侧一样,老的Array的比新的Array长 - 删除老节点

image.png
测试组件

  1. /* example/update/patchChildren/ArrayToArray.js */
  2. // 当老的Array 比 新的 Array长时 , -> 进行删除逻辑
  3. // 5. 基于左侧对比 -> Array1 比 Array2 长 执行删除 老节点逻辑
  4. const prevChildren = [
  5. h("p", { key: "A" }, "A"),
  6. h("p", { key: "B" }, "B"),
  7. h("p", { key: "C" }, "C"),
  8. ];
  9. const nextChildren = [
  10. h("p", { key: "A" }, "A"),
  11. h("p", { key: "B" }, "B"),
  12. ];

实现逻辑

  1. 执行删除的逻辑
  2. 左侧对比 Array1 Array2 执行删除 老节点逻辑
  3. * 3. Array1 Array2
  4. * - Array1: A B C
  5. * - Array2: A B
  6. * - 基于左侧对比, 找到乱序元素 C , 执行删除 C
  7. *
  8. * 实现:
  9. * 1. 两个Array 进行对比,基于左侧对比, 因为是左侧一样,老的比新的长,多出了 C 节点,所以需要删除 C 节点
  10. * 2. 在进行左侧对比时, 基于指针 i e1 e2 , 确定 C节点的位置
  11. * 3. 最后指针为 i = 2 | e1 = 2 | e2 = 1 , 确定多出了 C 节点
  12. * 4. 执行删除 C 节点
  13. *
  14. * - 进行判断删除的逻辑
  15. * - i > e2 && i <= e1 时,执行删除逻辑 (主要是 i > e2 && i <= e1))
  16. * - 删除 C 节点

patchKeyedChildren 逻辑中操作节点的逻辑 , 中增加判断

  1. /* renderer.ts */
  2. if (i > e1) {
  3. if (i <= e2) {
  4. // 创建节点逻辑
  5. }
  6. } else if (i > e2) { // 增加判断逻辑
  7. // 执行删除的逻辑
  8. // 左侧对比 Array1 比 Array2 执行删除 老节点逻辑
  9. /**
  10. * 3. Array1 比 Array2 长
  11. * - Array1: A B C
  12. * - Array2: A B
  13. * - 基于左侧对比, 找到乱序元素 C , 执行删除 C
  14. *
  15. * 实现:
  16. * 1. 两个Array 进行对比,基于左侧对比, 因为是左侧一样,老的比新的长,多出了 C 节点,所以需要删除 C 节点
  17. * 2. 在进行左侧对比时, 基于指针 i e1 e2 , 确定 C节点的位置
  18. * 3. 最后指针为 i = 2 | e1 = 2 | e2 = 1 , 确定多出了 C 节点
  19. * 4. 执行删除 C 节点
  20. *
  21. * - 进行判断删除的逻辑
  22. * - i > e2 && i <= e1 时,执行删除逻辑 (主要是 i > e2 && i <= e1))
  23. * - 删除 C 节点
  24. */
  25. // 循环删除
  26. while (i <= e1) {
  27. // 执行删除多出的节点 c1[i].el 使用 hostRemove 接口删除
  28. hostRemove(c1[i].el)
  29. // 移动 i 的指针
  30. i++
  31. }
  32. } else {
  33. // 乱序的部分, 中间的部分
  34. }

image.png

右侧一样,老的Array的比新的Array长 - 删除老节点

image.png
组件模拟

  1. /* example/update/patchChildren/ArrayToArray.js */
  2. // 当老的Array 比 新的 Array长时 , -> 进行删除逻辑
  3. // 6. 基于右侧对比
  4. const prevChildren = [
  5. h("p", { key: "A" }, "A"),
  6. h("p", { key: "B" }, "B"),
  7. h("p", { key: "C" }, "C"),
  8. ];
  9. const nextChildren = [
  10. h("p", { key: "B" }, "B"),
  11. h("p", { key: "C" }, "C"),
  12. ];

实现逻辑

  1. 4. Array1 Array2
  2. - Array1: A B C
  3. - Array2: B C
  4. - 基于左侧对比, 得到变化的元素 A 执行删除 A
  5. - 基于右侧对比, 得到指针数据, i = 0 | e1 = 0 | e2 = -1
  6. - 发现于左侧对比,实现的判断一样 i <= e1 ; i > e2 执行删除逻辑
  7. - 删除 首部节点 A

所以不需要写代码, 实现的基本和 左侧对比 删除一样
image.png

6.5 diff 算法 - 中间对比

image.png

  1. // 两个Array 中间 进行对比
  2. 双端对比中, 最复杂的中间元素的对比, 也是 双端对比的最核心部分
  3. 中间元素乱序的几种情况
  4. Array1: A B C Y E F G
  5. Array2: A B E D C F G
  6. 中间乱序: C Y E -> E D C
  7. 1. 创建新的 D -> 老的节点中不存在, 新的节点中存在
  8. 2. 删除老的节点中 Y -> 在老节点的里面存在, 新节点里面不存在
  9. 3. 移动 C E -> 节点存在于新的和老的节点里, 但是位置发生改变了

6.5.1 中间对比 - 删除老节点

组件测试代码

  1. /* example/update/patchChildren/ArrayToArray.js */
  2. const prevChildren = [
  3. h("p", { key: "A" }, "A"),
  4. h("p", { key: "B" }, "B"),
  5. h("p", { key: "C", id: "c-prev" }, "C"),
  6. h("p", { key: "D" }, "D"),
  7. h("p", { key: "F" }, "F"),
  8. h("p", { key: "G" }, "G"),
  9. ];
  10. const nextChildren = [
  11. h("p", { key: "A" }, "A"),
  12. h("p", { key: "B" }, "B"),
  13. h("p", { key: "E" }, "E"), // 增加了E节点 & 删除 D 节点
  14. h("p", { key: "C", id: "c-next" }, "C"), // props不同
  15. h("p", { key: "F" }, "F"),
  16. h("p", { key: "G" }, "G"),
  17. ];

实现中间乱序节点, 删除的逻辑

  1. // 1. 删除Array2 中的节点 D -> 老节点中存在, 新节点中不存在 (移动|创建先不实现)
  2. Array1: A B C D F G
  3. Array2: A B E C F G
  4. Array1 Array2 已经进行过双端对比,得到中间节点的不同, 乱序的节点
  5. - C D
  6. - E C
  7. - 删除 D 并且创建 E
  8. 实现思路:找到中间乱序的节点, 进行遍历一遍 -> 在新Array2中创建映射表,遍历老Array1, 根据映射表,删除映射表的节点 -> D 节点
  9. - 还有一种遍历方法,也就是通过 key 查找节点。 -> 新节点key 是否在老节点 key 里面
  10. 1. 经过左侧与右侧的对比,得到中间节点的不同, 乱序的节点
  11. - 此时的指针为 i = 2 | e1 = 3 | e2 = 3
  12. 2. 创建 基于新Array2映射表, key -> value, key 就是节点的key, value 为节点的索引值
  13. 3. 老节点遍历,根据key 查找对应的映射表, 如果没有对应上,就删除老Array1 节点-> D 节点
  14. - 如果查找到对应的 映射表,调用 patch() 进行对比,可以 props | children不同需要进行修改

具体代码实现

  1. /* renderer.ts*/
  2. {
  3. // 其他代码
  4. } else {
  5. // 乱序的部分, 中间的部分
  6. // 1. 删除Array2 中的节点 D -> 老节点中存在, 新节点中不存在 (移动|创建先不实现)
  7. /**
  8. * Array1: A B C D F G
  9. * Array2: A B E C F G
  10. *
  11. *
  12. *
  13. * 在Array1 和 Array2 已经进行过双端对比,得到中间节点的不同, 乱序的节点
  14. * - C D
  15. * - E C
  16. * - 删除 D 并且创建 E
  17. *
  18. * 实现思路:找到中间乱序的节点, 进行遍历一遍 -> 在新Array2中创建映射表,遍历老Array1, 根据映射表,删除映射表的节点 -> D 节点
  19. * - 还有一种遍历方法,也就是通过 key 查找节点。 -> 新节点key 是否在老节点 key 里面
  20. *
  21. * 1. 经过左侧与右侧的对比,得到中间节点的不同, 乱序的节点
  22. * - 此时的指针为 i = 2 | e1 = 3 | e2 = 3
  23. * 2. 创建 基于新Array2映射表, key -> value, key 就是节点的key, value 为节点的索引值
  24. * 3. 老节点遍历,根据key 查找对应的映射表, 如果没有对应上,就删除老Array1中 节点-> D 节点
  25. * - 如果查找到对应的 映射表,调用 patch() 进行对比,可以 props | children不同需要进行修改
  26. */
  27. // 此时的指针为 i = 2 | e1 = 3 | e2 = 3
  28. // 1. 需要遍历 Array1 和 Array2 中间乱序的部分
  29. // 创建指针
  30. let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置
  31. let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置
  32. // 2. 基于 新Array2 创建映射表, 存储Array2 中间乱序的节点部分
  33. const keyToNewIndexMap = new Map<string, number>()
  34. for (let i = s2; i <= e2; i++) {
  35. // 获取到当前节点
  36. const nextChild = c2[i]
  37. // 添加到映射表中, key -> value , key 就是节点的key, value 为节点的索引值
  38. keyToNewIndexMap.set(nextChild.key, i)
  39. }
  40. // 定义一个遍历,保存 Array1 节点 在 Array2 中的位置
  41. let newIndex
  42. // 3. 遍历 Array1 中间乱序的部分
  43. for (let i = s1; i <= e1; i++) {
  44. // 获取到 Array1 当前的节点
  45. const prevChild = c1[i]
  46. // 判断 Array1 当前节点 的属性是否具有key
  47. // 当前节点 没有key, 那么key就有两种 情况 null | undefined
  48. if (prevChild.key !== null) {
  49. // 当有key 时,才会去判断是否在映射表中, 因为映射表基于 Array2 乱序节点 key 创建的
  50. // 根据在老Array 真正遍历的节点 key, 查找对应的映射表
  51. // 如果能获取到值 -> 新Array2中 一定存在该节点
  52. // 如果没有 -> 说明该节点在新Array2中不存在,需要删除
  53. newIndex = keyToNewIndexMap.get(prevChild.key) // 返回索引值, 新节点所在位置
  54. // 当 key 没有时 newIndex 为 undefined
  55. } else {
  56. // 如果没有 当前节点 没有设置 key
  57. // 遍历 新Array2 中的节点,对比 Array1 中的节点, 判断是否存在
  58. // 如果不相同,删除老节点
  59. for (let j = s2; i <= e2; i++) {
  60. // 判断 新老节点是否相同
  61. if (isSomeVNodeType(prevChild, c2[j])) {
  62. // 进行 newIndex 赋值 -> 新节点所在的位置
  63. newIndex = j
  64. // 查找到节点, 就没有必要遍历了
  65. break
  66. }
  67. }
  68. }
  69. // 4. 基于 newIndex索引,判断节点的状态
  70. // 判断 newIndex 是否存在
  71. if (newIndex === undefined) {
  72. // 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除
  73. // 删除老节点 ,也就是当前节点
  74. hostRemove(prevChild.el)
  75. } else {
  76. // 如果 newIndex 具有值 -> 新节点所在的位置
  77. // 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性
  78. patch(prevChild, c2[newIndex], container, parentComponent, null)
  79. }
  80. }
  81. }

完成删除中间乱序的的节点 - (E 还没有创建)
image.png

优化删除的逻辑

  1. /* example/update/patchChildren/ArrayToArray.js */
  2. const prevChildren = [
  3. h("p", { key: "A" }, "A"),
  4. h("p", { key: "B" }, "B"),
  5. h("p", { key: "C", id: "c-prev" }, "C"),
  6. // 新增了 E 节点
  7. h("p", { key: "E" }, "E"),
  8. h("p", { key: "D" }, "D"),
  9. h("p", { key: "F" }, "F"),
  10. h("p", { key: "G" }, "G"),
  11. ];
  12. const nextChildren = [
  13. h("p", { key: "A" }, "A"),
  14. h("p", { key: "B" }, "B"),
  15. h("p", { key: "E" }, "E"), // 增加了E节点 & 删除 D 节点
  16. h("p", { key: "C", id: "c-next" }, "C"), // props不同
  17. h("p", { key: "F" }, "F"),
  18. h("p", { key: "G" }, "G"),
  19. ];

优化思路

  1. // 2. 优化删除逻辑
  2. * Array1: A B C E D F G
  3. * Array2: A B E C F G
  4. *
  5. * - i = 2 | e1 = 4 | e2 = 3
  6. * 优化的点: 当遍历Array1, 对应映射表时,如果映射表中的项已经映射完了,
  7. 就不再进行循环映射了,直接删除Array1 之后的节点
  8. * 实现:
  9. * 1. 记录新Array 变化的数量, 当每次映射时候,定义一个变量记录下来
  10. * 2. 判断记录的数量 超过 新节点的数量时,Array1 后面的所有节点都进行移除
  11. *

代码实现

  1. /* renderer.ts*/
  2. {
  3. // 其他代码
  4. } else {
  5. // 乱序的部分, 中间的部分
  6. let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置
  7. let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置
  8. // 定义一个变量 , 记录新Array2 中间乱序节点的数量
  9. const toBePatched = e2 - s2 + 1 // 3 - 2 + 1 -> 新Array2 乱序节点的数量
  10. // 定义一个变量 记录新Array1 已经映射过的节点数量 当每次映射时候,定义一个变量记录下来
  11. let patched = 0 // Array1 已经映射过节点的数量
  12. // 2. 基于 新Array2 创建映射表, 存储Array2 中间乱序的节点部分
  13. const keyToNewIndexMap = new Map<string, number>()
  14. for (let i = s2; i <= e2; i++) {
  15. // 获取到当前节点
  16. const nextChild = c2[i]
  17. // 添加到映射表中, key -> value , key 就是节点的key, value 为节点的索引值
  18. keyToNewIndexMap.set(nextChild.key, i)
  19. }
  20. // 定义一个遍历,保存 Array1 节点 在 Array2 中的位置
  21. let newIndex
  22. // 3. 遍历 Array1 中间乱序的部分
  23. for (let i = s1; i <= e1; i++) {
  24. // 获取到 Array1 当前的节点
  25. const prevChild = c1[i]
  26. // 这里每次都需要去检查 patched 是否超过 toBePatched,如果超过,就不再进行循环了
  27. // patched 映射的次数 >= toBePatched 新节点的数量, 如果超过了,直接删除老节点
  28. if (patched >= toBePatched) {
  29. // 直接删除了,没必要执行下面的逻辑
  30. hostRemove(prevChild.el)
  31. continue
  32. }
  33. // 判断 Array1 当前节点 的属性是否具有key
  34. // 当前节点 没有key, 那么key就有两种 情况 null | undefined
  35. if (prevChild.key !== null) {
  36. // 当有key 时,才会去判断是否在映射表中, 因为映射表基于 Array2 乱序节点 key 创建的
  37. // 根据在老Array 真正遍历的节点 key, 查找对应的映射表
  38. // 如果能获取到值 -> 新Array2中 一定存在该节点
  39. // 如果没有 -> 说明该节点在新Array2中不存在,需要删除
  40. newIndex = keyToNewIndexMap.get(prevChild.key) // 返回索引值, 新节点所在位置
  41. // 当 key 没有时 newIndex 为 undefined
  42. } else {
  43. // 如果没有 当前节点 没有设置 key
  44. // 遍历 新Array2 中的节点,对比 Array1 中的节点, 判断是否存在
  45. // 如果不相同,删除老节点
  46. for (let j = s2; i <= e2; i++) {
  47. // 判断 新老节点是否相同
  48. if (isSomeVNodeType(prevChild, c2[j])) {
  49. // 进行 newIndex 赋值 -> 新节点所在的位置
  50. newIndex = j
  51. // 查找到节点, 就没有必要遍历了
  52. break
  53. }
  54. }
  55. }
  56. // 4. 基于 newIndex索引,判断节点的状态
  57. // 判断 newIndex 是否存在
  58. if (newIndex === undefined) {
  59. // 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除
  60. // 删除老节点 ,也就是当前节点
  61. hostRemove(prevChild.el)
  62. } else {
  63. // 如果 newIndex 具有值 -> 新节点所在的位置
  64. // 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性
  65. patch(prevChild, c2[newIndex], container, parentComponent, null)
  66. // 这里表示已经映射完一个节点了,需要在这里赋值 patched + 1
  67. patched++
  68. }
  69. }
  70. }

改完代码, 根据DEMO,当Array1 遍历到 D 节点时, Array2创建的映射表, 已经映射完了,所以 D 节点直接删除,不会执行深层的逻辑

6.5.2 中间对比 - 节点的移动

测试组件

  1. // 中间节点 - 移动 - 节点存在于新的和老的里面,但是位置变了
  2. // a b (c d e) f g
  3. // a b (e c d) f g
  4. // 最长递增子序列 [1, 2] -> c d
  5. const prevChildren = [
  6. h("p", { key: "A" }, "A"),
  7. h("p", { key: "B" }, "B"),
  8. h("p", { key: "C" }, "C"),
  9. h("p", { key: "D" }, "D"),
  10. h("p", { key: "E" }, "E"),
  11. h("p", { key: "F" }, "F"),
  12. h("p", { key: "G" }, "G"),
  13. ];
  14. const nextChildren = [
  15. h("p", { key: "A" }, "A"),
  16. h("p", { key: "B" }, "B"),
  17. h("p", { key: "E" }, "E"), // E 节点移动
  18. h("p", { key: "C" }, "C"),
  19. h("p", { key: "D" }, "D"),
  20. h("p", { key: "F" }, "F"),
  21. h("p", { key: "G" }, "G"),
  22. ];
  23. // 3. 中间节点移动节点的逻辑
  24. /**
  25. * 1. 确定中间节点的不同,并且建立中间节点, 新Array 与 老Array 的映射关系
  26. * 2. 初始化映射表
  27. * 3. 赋值映射表
  28. * 4. 求最长递增子序列, 也就是新节点中不变的节点,一共有多少个
  29. * 5. 进行两个节点的对比
  30. */

代码实现

  1. /* renderer.ts */
  2. else {
  3. // 1. 需要遍历 Array1 和 Array2 中间乱序的部分
  4. // 创建指针
  5. let s1 = i // 老Array1 中间乱序的部分,开始遍历的位置
  6. let s2 = i // 新 Array2 中间乱序的部分,开始遍历的位置
  7. // 定义一个变量 , 记录新Array2 中间乱序节点的数量
  8. const toBePatched = e2 - s2 + 1 // 3 - 2 + 1 -> 新Array2 乱序节点的数量
  9. // 定义一个变量 记录新Array1 已经映射过的节点数量 当每次映射时候,定义一个变量记录下来
  10. let patched = 0 // Array1 已经映射过节点的数量
  11. let moved = false
  12. let maxNewIndexSofar = 0
  13. // 3. 中间节点移动节点的逻辑
  14. /**
  15. * 1. 确定中间节点的不同,并且建立中间节点, 新Array 与 老Array 的映射关系
  16. * 2. 初始化映射表
  17. * 3. 赋值映射表
  18. * 4. 求最长递增子序列, 也就是新节点中不变的节点,一共有多少个
  19. * 5. 进行两个节点的对比
  20. */
  21. // 3.1. 建立映射表 新Array 与 老Array 的映射关系
  22. // 给定一个定长的数组, 因为性能更好,数组范围是,Array2 中间节点变化的数量
  23. const newIndexToOldIndexMap = new Array(toBePatched)
  24. // 3.2. 初始化 映射表, 赋值为 i, i -> Array1 & Array2 的开始遍历位置
  25. // 表示没有建立映射关系
  26. // 循环Array2, 初始化这个映射表
  27. for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
  28. // 其他代码
  29. // 判断 newIndex 是否存在
  30. if (newIndex === undefined) {
  31. // 如果没有取到值 -> undefined,说明该节点在新Array2中不存在,需要删除
  32. // 删除老节点 ,也就是当前节点
  33. hostRemove(prevChild.el)
  34. } else {
  35. // 3.3 判断
  36. if (newIndex >= maxNewIndexSofar) {
  37. maxNewIndexSofar = newIndex
  38. } else {
  39. moved = true
  40. }
  41. // 这里赋值这个映射表
  42. newIndexToOldIndexMap[newIndex - s2] = i + 1
  43. // 如果 newIndex 具有值 -> 新节点所在的位置
  44. // 调用patch() 进行更深度的对比 -> 进行判断 props | children 这些属性
  45. patch(prevChild, c2[newIndex], container, parentComponent, null)
  46. // 这里表示已经映射完一个节点了,需要在这里赋值 patched + 1
  47. patched++
  48. }
  49. // 4. 获取最长递增子序列
  50. const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []
  51. // 5. 进行节点的对比, 一个是新节点,一个是最长递增子序列
  52. // 定义索引指针, 用于指向最长递增子序列
  53. let j = increasingNewIndexSequence.length - 1
  54. // 遍历 Array2
  55. for (let i = toBePatched - 1; i >= 0; i--) {
  56. // 调用 insert() 方法 -> 基于锚点
  57. const nextIndex = i + s2
  58. const nextChild = c2[nextIndex]
  59. // 锚点
  60. const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
  61. // 创建节点逻辑
  62. if (newIndexToOldIndexMap[i] === 0) {
  63. patch(null, nextChild, container, parentComponent, anchor);
  64. } else if (moved) { // 判断移动
  65. if (j < 0 || i !== increasingNewIndexSequence[j]) {
  66. // 进行移动位置
  67. // console.log("移动位置")
  68. hostInsert(nextChild.el, container, anchor)
  69. } else {
  70. // 不需要移动
  71. j--
  72. }
  73. }
  74. }
  75. }

求最长递增子序列

  1. /* renderer.ts */
  2. // 根据映射表,求最长递增子序列
  3. function getSequence(arr) {
  4. const p = arr.slice();
  5. const result = [0];
  6. let i, j, u, v, c;
  7. const len = arr.length;
  8. for (i = 0; i < len; i++) {
  9. const arrI = arr[i];
  10. if (arrI !== 0) {
  11. j = result[result.length - 1];
  12. if (arr[j] < arrI) {
  13. p[i] = j;
  14. result.push(i);
  15. continue;
  16. }
  17. u = 0;
  18. v = result.length - 1;
  19. while (u < v) {
  20. c = (u + v) >> 1;
  21. if (arr[result[c]] < arrI) {
  22. u = c + 1;
  23. } else {
  24. v = c;
  25. }
  26. }
  27. if (arrI < arr[result[u]]) {
  28. if (u > 0) {
  29. p[i] = result[u - 1];
  30. }
  31. result[u] = i;
  32. }
  33. }
  34. }
  35. u = result.length;
  36. v = result[u - 1];
  37. while (u-- > 0) {
  38. result[u] = v;
  39. v = p[v];
  40. }
  41. return result;
  42. }

E 节点的移动
image.png

6.5.3 中间对比 - 创建节点

  1. // 3. 创建新的节点
  2. // a,b,(c,e),f,g
  3. // a,b,(e,c,d),f,g
  4. // d 节点在老的节点中不存在,新的里面存在,所以需要创建
  5. const prevChildren = [
  6. h("p", { key: "A" }, "A"),
  7. h("p", { key: "B" }, "B"),
  8. h("p", { key: "C" }, "C"),
  9. h("p", { key: "E" }, "E"),
  10. h("p", { key: "F" }, "F"),
  11. h("p", { key: "G" }, "G"),
  12. ];
  13. const nextChildren = [
  14. h("p", { key: "A" }, "A"),
  15. h("p", { key: "B" }, "B"),
  16. h("p", { key: "E" }, "E"), // 移动 E 节点
  17. h("p", { key: "C" }, "C"),
  18. h("p", { key: "D" }, "D"), // 创建 D 节点
  19. h("p", { key: "F" }, "F"),
  20. h("p", { key: "G" }, "G"),
  21. ];

代码实现

  1. /* renderer.ts */
  2. // 4. 获取最长递增子序列
  3. const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []
  4. // 5. 进行节点的对比, 一个是新节点,一个是最长递增子序列
  5. // 定义索引指针, 用于指向最长递增子序列
  6. let j = increasingNewIndexSequence.length - 1
  7. // 遍历 Array2
  8. for (let i = toBePatched - 1; i >= 0; i--) {
  9. // 调用 insert() 方法 -> 基于锚点
  10. const nextIndex = i + s2
  11. const nextChild = c2[nextIndex]
  12. // 锚点
  13. const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
  14. // 创建节点逻辑
  15. if (newIndexToOldIndexMap[i] === 0) {
  16. patch(null, nextChild, container, parentComponent, anchor);
  17. } else if (moved) { // 判断移动
  18. if (j < 0 || i !== increasingNewIndexSequence[j]) {
  19. // 进行移动位置
  20. // console.log("移动位置")
  21. hostInsert(nextChild.el, container, anchor)
  22. } else {
  23. // 不需要移动
  24. j--
  25. }
  26. }
  27. }

image.png