Skip to content

Commit

Permalink
267
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Dec 26, 2022
1 parent 195435b commit 7236fe3
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./前沿技术/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">266.精读《磁贴布局 - 功能实现》</a>
最新精读:<a href="./前沿技术/267.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E3%80%8B.md">267.精读《磁贴布局 - 性能优化》</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -205,6 +205,7 @@
- <a href="./前沿技术/264.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%BB%B4%E6%8A%A4%E5%A5%BD%E4%B8%80%E4%B8%AA%E5%A4%8D%E6%9D%82%E9%A1%B9%E7%9B%AE%E3%80%8B.md">264.精读《维护好一个复杂项目》</a>
- <a href="./前沿技术/265.%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%88%86%E6%9E%90%E3%80%8B.md">265.精读《磁贴布局 - 功能分析》</a>
- <a href="./前沿技术/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">266.精读《磁贴布局 - 功能实现》</a>
- <a href="./前沿技术/267.%E7%B2%BE%E8%AF%BB%E3%80%8A%E7%A3%81%E8%B4%B4%E5%B8%83%E5%B1%80%20-%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E3%80%8B.md">267.精读《磁贴布局 - 性能优化》</a>

### TS 类型体操

Expand Down
172 changes: 172 additions & 0 deletions 前沿技术/267.精读《磁贴布局 - 性能优化》.md
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)

0 comments on commit 7236fe3

Please sign in to comment.