2

经过上一篇 精读《磁贴布局 - 功能实现》 的介绍,这次我们进入性能优化环节。

精读

磁贴布局性能优化方式有很多,比如通过空间换时间,存储父子关系的索引,方便快速查找到目标组件。但有一个最核心的性能优化点,即碰撞性能优化。

试想,最朴素的判断组件碰撞方法是什么?一般会遍历画布所有的组件,根据当前组件位置与目标组件位置的相对位置判断是否产生碰撞,所以仅判断单个组件碰撞时,时间复杂度是 O(n)。

但磁贴布局的碰撞判断涉及整个画布,因为一个组件的移动可能引发另一个组件的移动,形成一系列连环布局变化,比如下面这个情况:

         [---]
         [   ]
         [ A ]
         [   ]
     ↑   [---]
[---------]
[    B    ]
[---------]
  [---]
  [ C ]
  [---]
   [-------]
   [   D   ]
   [-------]

比如将 B 向上移动,每个组件落下来时都要做独立的碰撞判定。因为最终碰撞结果是很难预测的,只能一个组件一个组件的判断。比如上面的例子,结果如下:

[---------]
[    B    ]
[---------]
  [---]  [---]
  [ C ]  [   ]
  [---]  [ A ]
         [   ]
         [---]
   [-------]
   [   D   ]
   [-------]

可以看到,D 本来是紧紧靠着 C 的,但因为 A 组件移下来了,且 A 比 C 高,所以 D 紧靠的组件就从 C 变成 A 了,这个在 C 做独立碰撞判断之前,是难以通过画布的结构分析出来的,更不用说结合上画布的整体大小缩放、栅格数量的变化后产生的影响,组件最终落点必须每个组件通过正确顺序依次判定碰撞后才能确定。

因此磁贴碰撞的时间复杂度是 O(n²),比如页面中有 100 个组件,就至少要遍历 10000 次才能完成一次布局计算,这样在比较极限的情况下,比如页面有 1000 个组件时,布局计算肯定非常耗时。

栅格碰撞判定法

再思考一个问题,正是由于磁贴布局的碰撞判定,导致 磁贴布局不可能存在组件重叠的情况,因此即便画布存在 1000 个组件,只要组件宽高不是特别小(比如每个组件 1px 宽高,挤满 1000px 区域),都不可能聚集在某个小区域内,而是分散在很大的范围,那么与当前组件过远的组件就根本不需要做碰撞判定,因为他们不可能相交。

再类比到人判断碰撞的视角,当画布有 1000 个组件时,我们也能一眼看出来某个组件与哪些组件相交,但这个判断来自于肉眼在可视区域一扫而过,而不是把 1000 个组件全部看一遍。这说明人眼判定碰撞是经过优化的:以这个组件为圆心,上下左右扩大一定的范围扫一眼是否有碰撞就够了。

因此我们模拟人眼找碰撞的思路,把画布分为若干的栅格,记录每个组件所在的栅格,这样碰撞判定时,只要在组件所在栅格内进行判定就行了。

如下将画布分为若干栅格:

  [---] │        │        │        │
  [ A ] │        │        │        │
  [---] │        │        │        │
────────┼────────┼────────┼────────┼────────
     [-----]     │        │        │
     [  B  ]     │   [---]│        │
     [-----][C]  │   [ G ]│        │
────────┼────────┼───[---]┼────────┼────────
        │        │   [E]  │   [F]  │
        │        │   [-----------] │
        │        │   [           ] │
────────┼────────┼───[     D     ]─┼────────
        │        │   [           ] │
        │        │   [-----------] │
        │        │        │        │

这样当判定如下组件碰撞时,要对比的组件如下:

  • A:对比组件无。
  • B:对比组件 C。
  • D:对比组件 E、F、G

由于一个区域承载组件数量是固定的,所以 O(n²) 时间复杂度就优化为了 O(n x P) 其中 P 对每个组件来说都是常数,因此时间复杂度最终为 O(n)。

当然这里存在几个注意事项:

  1. 需要空间换时间,即存储每个组件属于哪些区域,以及每个区域有哪些组件,这样拖拽判定时无需遍历所有组件。
  2. 栅格大小不宜过大,栅格过大则划分栅格的意义就不大了,因为一个栅格内组件数还是很多。
  3. 栅格大小不宜过小,这样每个组件可能横跨很多栅格,导致栅格数量本身的循环次数甚至会超越组件树,就变成了负优化。

关于栅格大小,一般磁贴布局会设置 cols rowHeight 两个选项,以这两个选项的正整数倍为跨度设置栅格是比较合适的,这样会尽可能减少栅格的无效面积。

不同场景下的栅格计算

上面说了 组件碰撞 如何使用栅格计算,我们再总结一下:判定组件碰撞,只要找到当前组件所在的栅格 areas,遍历每一个栅格区域内的组件即可。

除了碰撞判断外,磁贴拖拽过程中还有两个场景需要计算组件间碰撞关系,主要包括 落点位置落点后组件排序 两个场景。

比如下面的例子:

<img width=200 src="https://user-images.githubusercontent.com/7970947/209458368-80dcd2b4-b6ee-4df9-adb7-271171352844.png">

蓝色框为鼠标拖动组件时,鼠标的实时位置,而红色背景正方形表示 落点位置,红色正方形下方的组件属于 落点后组件,这些组件因为红色正方形的位置插入,需要重新计算位置。

为了最大程度利用栅格优化性能,这两种情况需要分别判断。

落点位置

由于磁贴布局的重力是垂直向上的,因此落点只会落在当前组件的上方,也就是落点只会与上方组件碰撞,因此考虑垂直向上的栅格区域即可。而且过程中还是可以优化的,即一格一格向上查找,只要在某个格内查到碰撞组件,就可以终止查找了:

  [---] │        │
  [ A ] │        │
  [---] │        │
────────┼────────┼─────────
     [-----]     │
     [  B  ]     │
     [-----]     │
────────┼────────┼─────────
[-----] │        │
[  C  ] │        │
[-----] │        │
────────┼────────┼─────────
    [-----]      │
    [  D  ]      │
    [-----]      │

如上面的例子,移动 D 时:

  1. 先考虑 D 所在区域是否有组件垂直区域可碰撞,因为 D 所在区域只有自己,所以跳过。
  2. 在考虑 D 区域上方一格区域,发现组件 C,且与 D 在垂直位置可碰撞,因此 D 的落点位置放在 C 的下方。
  3. 查找结束,再向上的区域直接跳过。

因此落点位置的查找时间复杂度是 O(1)。

落点后组件排序

落点位置决定后,由于落点位置毕竟发生了变化,落点之后的组件都要重新按照磁贴向上的重力作用排序,所以此时组件查找范围是包含落点所在区域内,垂直向下的所有区域:

  [---] │        │
  [ A ] │        │
  [---] │        │
────────┼────────┼─────────
     [-----]     │
     [  B  ]     │
     [-----]     │
────────┼────────┼─────────
[-----] │        │
[  C  ] │        │
[-----] │        │
────────┼────────┼─────────
    [-----]      │
    [  D  ]      │
    [-----]      │

如上面的例子,移动 A 时,A 所在区域下方所有区域都要重新判断落点,也就是 B、C、D 组件所在区域。其他区域不受影响。我们假设所有组件均匀的平铺在所有区域,那么最坏的情况下(移动的组件在最顶部,那么一整条高度的区域都要搜索)纵向区域的组件数是 logn,所以时间复杂度理论上是 O(logn)。但一般情况磁贴布局高度远大于宽度,所以可能往较坏的 O(n) 复杂度发展,但不论如何,这个线性性能是可接受的。

总结

经过优化,磁贴布局在拖拽前、中、后各个阶段的计算复杂度均为 O(n),即一个拥有 500 个组件实例的复杂画布,也只要在每次拖动时循环 500 次计算位置,而配合空间换时间的一些 Map 映射关系配合,500 次计算加起来最多消耗 2~3 ms,而 1000 个组件实例也最多 4~6 ms 的消耗,但超过 1000 个组件实例的画布几乎是不可能存在的,况且这里 log(n) 的 n 指的是每个容器内的组件,因此只要单个容器内组件数量几乎不会超过特别多,所以性能是没有问题的。

讨论地址是:精读《磁贴布局 - 性能优化》· Issue #461 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

黄子毅
7k 声望9.5k 粉丝