经过上一篇 精读《磁贴布局 - 功能实现》 的介绍,这次我们进入性能优化环节。
磁贴布局性能优化方式有很多,比如通过空间换时间,存储父子关系的索引,方便快速查找到目标组件。但有一个最核心的性能优化点,即碰撞性能优化。
试想,最朴素的判断组件碰撞方法是什么?一般会遍历画布所有的组件,根据当前组件位置与目标组件位置的相对位置判断是否产生碰撞,所以仅判断单个组件碰撞时,时间复杂度是 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)。
当然这里存在几个注意事项:
- 需要空间换时间,即存储每个组件属于哪些区域,以及每个区域有哪些组件,这样拖拽判定时无需遍历所有组件。
- 栅格大小不宜过大,栅格过大则划分栅格的意义就不大了,因为一个栅格内组件数还是很多。
- 栅格大小不宜过小,这样每个组件可能横跨很多栅格,导致栅格数量本身的循环次数甚至会超越组件树,就变成了负优化。
关于栅格大小,一般磁贴布局会设置 cols
rowHeight
两个选项,以这两个选项的正整数倍为跨度设置栅格是比较合适的,这样会尽可能减少栅格的无效面积。
上面说了 组件碰撞 如何使用栅格计算,我们再总结一下:判定组件碰撞,只要找到当前组件所在的栅格 areas
,遍历每一个栅格区域内的组件即可。
除了碰撞判断外,磁贴拖拽过程中还有两个场景需要计算组件间碰撞关系,主要包括 落点位置 与 落点后组件排序 两个场景。
比如下面的例子:
蓝色框为鼠标拖动组件时,鼠标的实时位置,而红色背景正方形表示 落点位置,红色正方形下方的组件属于 落点后组件,这些组件因为红色正方形的位置插入,需要重新计算位置。
为了最大程度利用栅格优化性能,这两种情况需要分别判断。
由于磁贴布局的重力是垂直向上的,因此落点只会落在当前组件的上方,也就是落点只会与上方组件碰撞,因此考虑垂直向上的栅格区域即可。而且过程中还是可以优化的,即一格一格向上查找,只要在某个格内查到碰撞组件,就可以终止查找了:
[---] │ │
[ A ] │ │
[---] │ │
────────┼────────┼─────────
[-----] │
[ B ] │
[-----] │
────────┼────────┼─────────
[-----] │ │
[ C ] │ │
[-----] │ │
────────┼────────┼─────────
[-----] │
[ D ] │
[-----] │
如上面的例子,移动 D 时:
- 先考虑 D 所在区域是否有组件垂直区域可碰撞,因为 D 所在区域只有自己,所以跳过。
- 在考虑 D 区域上方一格区域,发现组件 C,且与 D 在垂直位置可碰撞,因此 D 的落点位置放在 C 的下方。
- 查找结束,再向上的区域直接跳过。
因此落点位置的查找时间复杂度是 O(1)。
落点位置决定后,由于落点位置毕竟发生了变化,落点之后的组件都要重新按照磁贴向上的重力作用排序,所以此时组件查找范围是包含落点所在区域内,垂直向下的所有区域:
[---] │ │
[ A ] │ │
[---] │ │
────────┼────────┼─────────
[-----] │
[ B ] │
[-----] │
────────┼────────┼─────────
[-----] │ │
[ C ] │ │
[-----] │ │
────────┼────────┼─────────
[-----] │
[ D ] │
[-----] │
如上面的例子,移动 A 时,A 所在区域下方所有区域都要重新判断落点,也就是 B、C、D 组件所在区域。其他区域不受影响。我们假设所有组件均匀的平铺在所有区域,那么最坏的情况下(移动的组件在最顶部,那么一整条高度的区域都要搜索)纵向区域的组件数是 logn,所以时间复杂度理论上是 O(logn)。但一般情况磁贴布局高度远大于宽度,所以可能往较坏的 O(n) 复杂度发展,但不论如何,这个线性性能是可接受的。
经过优化,磁贴布局在拖拽前、中、后各个阶段的计算复杂度均为 O(n),即一个拥有 500 个组件实例的复杂画布,也只要在每次拖动时循环 500 次计算位置,而配合空间换时间的一些 Map 映射关系配合,500 次计算加起来最多消耗 23 ms,而 1000 个组件实例也最多 46 ms 的消耗,但超过 1000 个组件实例的画布几乎是不可能存在的,况且这里 log(n) 的 n 指的是每个容器内的组件,因此只要单个容器内组件数量几乎不会超过特别多,所以性能是没有问题的。
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)