React Effects List大重構,是為了他?

卡頌發表於2021-12-03

大家好,我卡頌。

本文我們來看React內部Effects List機制重構的前因後果。

閱讀完本文,你可以掌握React18對比之前版本,Suspense特性的差異及原因。

歡迎加入人類高質量前端框架群,帶飛

什麼是副作用

簡易的React工作原理可以概括為:

  1. 觸發更新
  2. render階段:計算更新會造成的副作用
  3. commit階段:執行副作用

副作用包含很多型別,比如:

  • PlacementDOM節點的插入與移動
  • PassiveuseEffect回撥執行
  • ChildDeletion指移除子DOM節點
  • 等等

更新造成DOM變化主要就是PlacementChildDeletion在起作用。

那麼render階段如何儲存副作用commit階段又是如何使用副作用的呢?

Effects List

在重構前,render階段,帶有副作用的節點會連線形成連結串列,這條連結串列被稱為Effects List

比如下圖,B、C、E存在副作用,連線形成Effects List

commit階段不需要從A向下遍歷整棵樹,只需要遍歷Effects List就能找到所有有副作用的節點並執行對應操作。

SubtreeFlags

在重構之後,會將子節點的副作用冒泡到父節點的SubtreeFlags屬性。

比如B、C、E包含的副作用如下圖:

冒泡流程如下:

  1. B的副作用Passive,冒泡到A,A.SubtreeFlags包含Passive
  2. E的副作用Placement,冒泡到D,D.SubtreeFlags包含Placement
  3. D冒泡到C,C.SubtreeFlags包含Placement
  4. C的副作用UpdateC.SubtreeFlags包含Placement,C冒泡到A
  5. 最終A.SubtreeFlags包含PassivePlacementUpdate

這就代表A的子樹中包含這三種副作用。

commit階段,再根據SubtreeFlags一層層查詢有副作用的節點並執行對應操作。

可見,SubtreeFlags需要遍歷樹,而Effects List只需要遍歷連結串列,效率更高。那麼React為什麼要重構呢?

Suspense

答案是:SubtreeFlags遍歷子樹的操作雖然比Effects List需要遍歷更多節點,但是React18中一種新特性恰恰需要遍歷子樹

這個特性就是Suspense

Suspensev16就提供的功能,但v18之後,當開啟併發功能,Suspense與之前版本的行為是有區別的。

考慮如下元件:

<Suspense fallback={<h3>loading...</h3>}>
  <LazyCpn />
  <Sibling />
</Suspense>

其中LazyCpn是使用React.lazy包裹的非同步載入元件

Sibling程式碼如下:

function Sibling() {
  useEffect(() => {
    console.log("Sibling effect");
  }, []);

  return <h1>Sibling</h1>;
}

由於Suspense會等待子孫元件中的非同步請求完畢後再渲染,所以當程式碼執行時頁面首先會渲染fallback

<h3>loading...</h3>

但是Sibling並不是非同步的!這裡就體現了新舊版本React的差異。

新舊版React的差異

再回顧下開篇介紹的簡易React工作原理:

  1. 觸發更新
  2. render階段:協調器計算更新會造成的副作用
  3. commit階段:渲染器執行副作用

在開啟併發之前,React保證一次render階段對應一次commit階段

所以在上例中,雖然由於LazyCpn在請求導致Suspense渲染fallback,但是並不會阻止Sibling渲染,也不會阻止SiblinguseEffect的執行。

控制檯還是會列印Sibling effect

同時,為了在視覺上顯得Sibling沒有渲染,Sibling渲染的DOM節點會被設定display: none

但這其實挺hack的。畢竟根據Suspense的理念,如果子孫元件有非同步載入的內容,那應該只渲染fallback(而不是同時渲染display: none的內容)

所以在新版中,針對Suspense不顯示的子樹做了單獨的處理,既不會渲染display: none的內容,也不會執行useEffect回撥:

要實現這部分處理的基礎,就是改變commit階段遍歷的方式,也就回到開篇提到的Effects List重構為subtreeFlags

你可以從這個線上Demo直觀的感受新舊版Suspense的差異

總結

今天我們又學到了一個React原始碼小知識。

值得一提的是,針對Suspense的這次改進,為React帶來一種新的內部元件型別 —— Offscreen Component

未來他可能是實現Reactkeep-alive的基礎。

相關文章