經過上一篇 精讀《磁貼布局 - 功能實現》 的介紹,這次我們進入效能最佳化環節。
精讀
磁貼布局效能最佳化方式有很多,比如透過空間換時間,儲存父子關係的索引,方便快速查詢到目標元件。但有一個最核心的效能最佳化點,即碰撞效能最佳化。
試想,最樸素的判斷元件碰撞方法是什麼?一般會遍歷畫布所有的元件,根據當前元件位置與目標元件位置的相對位置判斷是否產生碰撞,所以僅判斷單個元件碰撞時,時間複雜度是 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
,遍歷每一個柵格區域內的元件即可。
除了碰撞判斷外,磁貼拖拽過程中還有兩個場景需要計算元件間碰撞關係,主要包括 落點位置 與 落點後元件排序 兩個場景。
比如下面的例子:
<img width=200 src="https://user-images.githubusercontent.com/7970947/209458368-80dcd2b4-b6ee-4df9-adb7-271171352844.png">
藍色框為滑鼠拖動元件時,滑鼠的實時位置,而紅色背景正方形表示 落點位置,紅色正方形下方的元件屬於 落點後元件,這些元件因為紅色正方形的位置插入,需要重新計算位置。
為了最大程度利用柵格最佳化效能,這兩種情況需要分別判斷。
落點位置
由於磁貼布局的重力是垂直向上的,因此落點只會落在當前元件的上方,也就是落點只會與上方元件碰撞,因此考慮垂直向上的柵格區域即可。而且過程中還是可以最佳化的,即一格一格向上查詢,只要在某個格內查到碰撞元件,就可以終止查詢了:
[---] │ │
[ 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 次計算加起來最多消耗 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 許可證)