经过上一篇 [精读《磁贴布局 - 功能实现》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/266.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0%E3%80%8B.md) 的介绍,这次我们进入性能优化环节。 ## 精读 磁贴布局性能优化方式有很多,比如通过空间换时间,存储父子关系的索引,方便快速查找到目标组件。但有一个最核心的性能优化点,即碰撞性能优化。 试想,最朴素的判断组件碰撞方法是什么?一般会遍历画布所有的组件,根据当前组件位置与目标组件位置的相对位置判断是否产生碰撞,所以仅判断单个组件碰撞时,时间复杂度是 O(n)。 但磁贴布局的碰撞判断涉及整个画布,因为一个组件的移动可能引发另一个组件的移动,形成一系列连环布局变化,比如下面这个情况: ```text [---] [ ] [ A ] [ ] ↑ [---] [---------] [ B ] [---------] [---] [ C ] [---] [-------] [ D ] [-------] ``` 比如将 B 向上移动,每个组件落下来时都要做独立的碰撞判定。因为最终碰撞结果是很难预测的,只能一个组件一个组件的判断。比如上面的例子,结果如下: ```text [---------] [ B ] [---------] [---] [---] [ C ] [ ] [---] [ A ] [ ] [---] [-------] [ D ] [-------] ``` 可以看到,D 本来是紧紧靠着 C 的,但因为 A 组件移下来了,且 A 比 C 高,所以 D 紧靠的组件就从 C 变成 A 了,这个在 C 做独立碰撞判断之前,是难以通过画布的结构分析出来的,更不用说结合上画布的整体大小缩放、栅格数量的变化后产生的影响,组件最终落点必须每个组件通过正确顺序依次判定碰撞后才能确定。 因此磁贴碰撞的时间复杂度是 O(n²),比如页面中有 100 个组件,就至少要遍历 10000 次才能完成一次布局计算,这样在比较极限的情况下,比如页面有 1000 个组件时,布局计算肯定非常耗时。 ### 栅格碰撞判定法 再思考一个问题,正是由于磁贴布局的碰撞判定,导致 **磁贴布局不可能存在组件重叠的情况**,因此即便画布存在 1000 个组件,只要组件宽高不是特别小(比如每个组件 1px 宽高,挤满 1000px 区域),都不可能聚集在某个小区域内,而是分散在很大的范围,那么与当前组件过远的组件就根本不需要做碰撞判定,因为他们不可能相交。 再类比到人判断碰撞的视角,当画布有 1000 个组件时,我们也能一眼看出来某个组件与哪些组件相交,但这个判断来自于肉眼在可视区域一扫而过,而不是把 1000 个组件全部看一遍。这说明人眼判定碰撞是经过优化的:以这个组件为圆心,上下左右扩大一定的范围扫一眼是否有碰撞就够了。 因此我们模拟人眼找碰撞的思路,把画布分为若干的栅格,记录每个组件所在的栅格,这样碰撞判定时,只要在组件所在栅格内进行判定就行了。 如下将画布分为若干栅格: ```text [---] │ │ │ │ [ 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`,遍历每一个栅格区域内的组件即可。 除了碰撞判断外,磁贴拖拽过程中还有两个场景需要计算组件间碰撞关系,主要包括 **落点位置** 与 **落点后组件排序** 两个场景。 比如下面的例子: 蓝色框为鼠标拖动组件时,鼠标的实时位置,而红色背景正方形表示 **落点位置**,红色正方形下方的组件属于 **落点后组件**,这些组件因为红色正方形的位置插入,需要重新计算位置。 为了最大程度利用栅格优化性能,这两种情况需要分别判断。 #### 落点位置 由于磁贴布局的重力是垂直向上的,因此落点只会落在当前组件的上方,也就是落点只会与上方组件碰撞,因此考虑垂直向上的栅格区域即可。而且过程中还是可以优化的,即一格一格向上查找,只要在某个格内查到碰撞组件,就可以终止查找了: ```text [---] │ │ [ A ] │ │ [---] │ │ ────────┼────────┼───────── [-----] │ [ B ] │ [-----] │ ────────┼────────┼───────── [-----] │ │ [ C ] │ │ [-----] │ │ ────────┼────────┼───────── [-----] │ [ D ] │ [-----] │ ``` 如上面的例子,移动 D 时: 1. 先考虑 D 所在区域是否有组件垂直区域可碰撞,因为 D 所在区域只有自己,所以跳过。 2. 在考虑 D 区域上方一格区域,发现组件 C,且与 D 在垂直位置可碰撞,因此 D 的落点位置放在 C 的下方。 3. 查找结束,再向上的区域直接跳过。 因此落点位置的查找时间复杂度是 O(1)。 #### 落点后组件排序 落点位置决定后,由于落点位置毕竟发生了变化,落点之后的组件都要重新按照磁贴向上的重力作用排序,所以此时组件查找范围是包含落点所在区域内,垂直向下的所有区域: ```text [---] │ │ [ 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](https://github.com/dt-fe/weekly/issues/461) **如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** > 关注 **前端精读微信公众号** > 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))