-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
174 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
经过上一篇 [精读《磁贴布局 - 功能实现》](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`,遍历每一个栅格区域内的组件即可。 | ||
|
||
除了碰撞判断外,磁贴拖拽过程中还有两个场景需要计算组件间碰撞关系,主要包括 **落点位置** 与 **落点后组件排序** 两个场景。 | ||
|
||
比如下面的例子: | ||
|
||
<img width=200 src="https://user-images.githubusercontent.com/7970947/209458368-80dcd2b4-b6ee-4df9-adb7-271171352844.png"> | ||
|
||
蓝色框为鼠标拖动组件时,鼠标的实时位置,而红色背景正方形表示 **落点位置**,红色正方形下方的组件属于 **落点后组件**,这些组件因为红色正方形的位置插入,需要重新计算位置。 | ||
|
||
为了最大程度利用栅格优化性能,这两种情况需要分别判断。 | ||
|
||
#### 落点位置 | ||
|
||
由于磁贴布局的重力是垂直向上的,因此落点只会落在当前组件的上方,也就是落点只会与上方组件碰撞,因此考虑垂直向上的栅格区域即可。而且过程中还是可以优化的,即一格一格向上查找,只要在某个格内查到碰撞组件,就可以终止查找了: | ||
|
||
```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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |