精讀《磁貼布局 - 效能最佳化》

黃子毅發表於2022-12-26

經過上一篇 精讀《磁貼布局 - 功能實現》 的介紹,這次我們進入效能最佳化環節。

精讀

磁貼布局效能最佳化方式有很多,比如透過空間換時間,儲存父子關係的索引,方便快速查詢到目標元件。但有一個最核心的效能最佳化點,即碰撞效能最佳化。

試想,最樸素的判斷元件碰撞方法是什麼?一般會遍歷畫布所有的元件,根據當前元件位置與目標元件位置的相對位置判斷是否產生碰撞,所以僅判斷單個元件碰撞時,時間複雜度是 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)。

當然這裡存在幾個注意事項:

  1. 需要空間換時間,即儲存每個元件屬於哪些區域,以及每個區域有哪些元件,這樣拖拽判定時無需遍歷所有元件。
  2. 柵格大小不宜過大,柵格過大則劃分柵格的意義就不大了,因為一個柵格內元件數還是很多。
  3. 柵格大小不宜過小,這樣每個元件可能橫跨很多柵格,導致柵格數量本身的迴圈次數甚至會超越元件樹,就變成了負最佳化。

關於柵格大小,一般磁貼布局會設定 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 時:

  1. 先考慮 D 所在區域是否有元件垂直區域可碰撞,因為 D 所在區域只有自己,所以跳過。
  2. 在考慮 D 區域上方一格區域,發現元件 C,且與 D 在垂直位置可碰撞,因此 D 的落點位置放在 C 的下方。
  3. 查詢結束,再向上的區域直接跳過。

因此落點位置的查詢時間複雜度是 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 許可證

相關文章