React Effects List大重構(gòu),是為了他?
大家好,我卡頌。
本文我們來看React內(nèi)部Effects List機制重構(gòu)的前因后果。
閱讀完本文,你可以掌握React18對比之前版本,Suspense特性的差異及原因。
什么是副作用
簡易的React工作原理可以概括為:
1.觸發(fā)更新
2.render階段:計算更新會造成的副作用
3.commit階段:執(zhí)行副作用
副作用包含很多類型,比如:
- Placement指DOM節(jié)點的插入與移動
- Passive指useEffect回調(diào)執(zhí)行
- ChildDeletion指移除子DOM節(jié)點
- 等等
更新造成DOM變化主要就是Placement、ChildDeletion在起作用。
那么render階段如何保存副作用,commit階段又是如何使用副作用的呢?
Effects List
在重構(gòu)前,render階段,帶有副作用的節(jié)點會連接形成鏈表,這條鏈表被稱為Effects List。
比如下圖,B、C、E存在副作用,連接形成Effects List:
commit階段不需要從A向下遍歷整棵樹,只需要遍歷Effects List就能找到所有有副作用的節(jié)點并執(zhí)行對應(yīng)操作。
SubtreeFlags
在重構(gòu)之后,會將子節(jié)點的副作用冒泡到父節(jié)點的SubtreeFlags屬性。
比如B、C、E包含的副作用如下圖:
冒泡流程如下:
- B的副作用為Passive,冒泡到A,A.SubtreeFlags包含Passive
- E的副作用為Placement,冒泡到D,D.SubtreeFlags包含Placement
- D冒泡到C,C.SubtreeFlags包含Placement
- C的副作用為Update,C.SubtreeFlags包含Placement,C冒泡到A
- 最終A.SubtreeFlags包含Passive、Placement、Update
這就代表A的子樹中包含這三種副作用。
在commit階段,再根據(jù)SubtreeFlags一層層查找有副作用的節(jié)點并執(zhí)行對應(yīng)操作。
可見,SubtreeFlags需要遍歷樹,而Effects List只需要遍歷鏈表,效率更高。那么React為什么要重構(gòu)呢?
Suspense
答案是:SubtreeFlags遍歷子樹的操作雖然比Effects List需要遍歷更多節(jié)點,但是React18中一種新特性恰恰需要「遍歷子樹」。
這個特性就是Suspense。
Suspense是v16就提供的功能,但v18之后,當開啟并發(fā)功能,Suspense與之前版本的行為是有區(qū)別的。
考慮如下組件:
- <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并不是異步的!這里就體現(xiàn)了新舊版本React的差異。
新舊版React的差異
再回顧下開篇介紹的簡易React工作原理:
- 觸發(fā)更新
- render階段:協(xié)調(diào)器計算更新會造成的副作用
- commit階段:渲染器執(zhí)行副作用
在開啟并發(fā)之前,React保證一次render階段對應(yīng)一次commit階段。
所以在上例中,雖然由于LazyCpn在請求導(dǎo)致Suspense渲染fallback,但是并不會阻止Sibling渲染,也不會阻止Sibling中useEffect的執(zhí)行。
控制臺還是會打印「Sibling effect」。
同時,為了在視覺上顯得Sibling沒有渲染,Sibling渲染的DOM節(jié)點會被設(shè)置display: none:
但這其實挺hack的。畢竟根據(jù)Suspense的理念,如果子孫組件有異步加載的內(nèi)容,那應(yīng)該只渲染fallback(而不是同時渲染display: none的內(nèi)容)
所以在新版中,針對Suspense內(nèi)「不顯示的子樹」做了單獨的處理,既不會渲染display: none的內(nèi)容,也不會執(zhí)行useEffect回調(diào):
要實現(xiàn)這部分處理的基礎(chǔ),就是改變commit階段遍歷的方式,也就回到開篇提到的Effects List重構(gòu)為subtreeFlags。
你可以從這個在線Demo[1]直觀的感受新舊版Suspense的差異
總結(jié)
今天我們又學到了一個React源碼小知識。
值得一提的是,針對Suspense的這次改進,為React帶來一種新的內(nèi)部組件類型 —— Offscreen Component。
未來他可能是實現(xiàn)React版keep-alive的基礎(chǔ)。
參考資料
[1]在線Demo:
https://codesandbox.io/s/frosty-currying-35olk?file=/src/App.js