React的操作系統(tǒng)夢,任重道遠
這篇文章包括如下內容:
簡要回顧下React從16~21年的迭代歷程
React為什么對新特性(Concurrent Mode)有這么大執(zhí)念
為什么當前社區(qū)項目/庫要升級到Concurrent Mode比較困難
迭代歷程回顧
React Core Team從16年開始改造React的核心模塊Reconciler(diff算法會在該模塊執(zhí)行)。
經過一年多的改造,將其從流程不可中斷的「遞歸實現(xiàn)」(被稱為Stack Reconciler)改為流程可中斷的「遍歷實現(xiàn)」(被稱為Fiber Reconciler)。
在此之后,基于Fiber Reconciler,實現(xiàn)了一套可以區(qū)分任務優(yōu)先級的機制,大體原理如下:
不同交互(用戶點擊交互/請求數(shù)據(jù)/用戶拖拽...)觸發(fā)的狀態(tài)更新(比如調用this.setState)會擁有不同優(yōu)先級,在源碼內對應一個時間戳變量expirationTime。
React會根據(jù)expirationTime的大小調度這些更新,最終實現(xiàn)的效果為:「用戶交互」觸發(fā)的更新會擁有更高的優(yōu)先級,先于「請求數(shù)據(jù)」觸發(fā)的更新。
高優(yōu)先級意味著該更新對DOM產生的影響會更快呈現(xiàn)在用戶面前。
在此之后,React Core Team發(fā)現(xiàn)基于expirationTime的調度算法雖然能滿足fiber樹的整體優(yōu)先級調度,但是不夠靈活(比如無法滿足局部fiber樹的優(yōu)先級調度(例如Suspense))。
具體原因見這篇文章:啟發(fā)式更新算法
所以去年React Core Team的Andrew Clark將expirationTime模型重構為以一個32位二進制的位代表優(yōu)先級的lane模型。
PR參見Initial Lanes implementation #18796[1]
如果你是個React重度用戶,讓你聊聊這些年React的重大變化,可能你會說:
- Context API重構
- Hooks
但從我們上面講到的內容來看,從16年到21年,React底層其實做了大量重構工作。
有人問:做了這么多重構,React開發(fā)者居然一點感知都沒有?
是的,即使當前穩(wěn)定版本的React底層已經支持時間切片、支持更智能的更新合并機制(batchedUpdates)。
但是React內部有很多裹腳布一樣的代碼讓新架構的行為表現(xiàn)的與老架構(Stack Reconciler)一致。
React Core Team的執(zhí)念
就像開發(fā)業(yè)務的開發(fā)者需要背負OKR,強如React Core Team成員,也會為OKR苦惱。
20年的React圣誕特輯,React Core team的Rachel Nabors小姐姐就在文章Inside the React Core team[2]中表示:
不能因為你沒有產出就代表你沒有價值(一把辛酸淚)。
作為視圖層的庫,在不開大腦洞的情況下,React能做的已經趨于極致了。
協(xié)程、并發(fā)這些操作系統(tǒng)中的概念被搬進React,函數(shù)式編程的理念也在React中落地(Hooks)。
React該何去何從?
React的靈魂人物、Hooks的作者、同時也是TC39成員Sebastian Markbåge給出的答案是:
向后、向BFF層發(fā)展
簡單的說:
在SSR領域,當前的實現(xiàn)方案還比較粗獷:
- 組件在服務端編譯成模版字符串(脫水)
- 前端渲染模版字符串
- 完成組件的可交互(注水)與余下的渲染
這樣的SSR方案粒度不夠細,如果Fiber Reconciler能將時間切片的粒度控制在組件級別,SSR的粒度為什么不能控制在組件級別呢?
要達到這個目標,起碼需要支持:
- 一套React組件的流式數(shù)據(jù)傳輸協(xié)議(區(qū)別于字符串模版)
- 前端能精確控制組件的狀態(tài)(加載中/加載失敗/加載成功),即Suspense特性
而Suspense特性依賴Concurrent Mode的時間切片特性。
沒有社區(qū)的大量庫接入Concurrent Mode,使時間切片成為默認配置,Sebastian Markbåge的遠大理想(OKR)無異于空中樓閣。
所以,當務之急是讓社區(qū)盡快跟上React升級的步伐。
升級Concurrent Mode的難點
當前社區(qū)大量React生態(tài)庫的邏輯都是基于如下React運行流程:
- 狀態(tài)更新 --> render --> 視圖渲染
如果React的運行流程變?yōu)椋?/p>
- 狀態(tài)更新 --> render(可暫停) --> 視圖渲染
- 或
- 狀態(tài)更新 --> render(中斷)--> 重新狀態(tài)更新 --> render(可暫停) --> 視圖渲染
會發(fā)生什么?
會發(fā)生一種被稱為tearing的現(xiàn)象,我們來舉個例子:
假設我們有一個變量externalSource,初始值為1。
1000ms后externalSource會變?yōu)?。
- let externalSource = 1;
- setTimeout(() => {
- externalSource = 2;
- }, 1000)
我們有個組件A,他渲染的DOM依賴于externalSource的值:
- function A() {
- return <p>{externalSource}</p>;
- }
在當前版本的React中,在我們的應用中組件樹的不同地方使用A組件,會出現(xiàn)某些地方的DOM是<p>1</p>,某些地方是<p>2</p>么?
答案是:不會。
因為當前React的如下運行流程是同步的:
- 狀態(tài)更新 --> render --> 視圖渲染
使externalSource變?yōu)?的setTimeout會在這個流程對應的task(宏認為)執(zhí)行完后再執(zhí)行。
但是當切換到Concurrent Mode:
- 狀態(tài)更新 --> render(可暫停) --> 視圖渲染
當render暫停時,瀏覽器獲得JS線程控制權,就會執(zhí)行使externalSource變?yōu)?的setTimeout。
這樣可能不同的A組件渲染出的p標簽內的數(shù)字不一樣。
這種由于React運行流程變化,導致依賴外部資源時,狀態(tài)與視圖不一致的現(xiàn)象,就是tearing。
這里改變externalSource的外力,可能來自于各種task(IO、setTimeout...)
- 當前有個解決外部資源狀態(tài)同步的提案useMutableSource[3]
- 這個庫will-this-react-global-state-work-in-concurrent-mode[4]測試了主流狀態(tài)管理庫是否會導致tearing
艱難的小步前進
為了讓開發(fā)者能漸進、少點痛苦的升級到Concurrent Mode,React Core Team一直在努力:
- 提供StrictMode(嚴格模式)組件,規(guī)范開發(fā)者行為
- 將componentWilXXX標記為unsaft_
- 提供漸進的升級路線,(從legacy模式到blocking模式到concurrent模式)
顯然,React Core Team覺得社區(qū)的升級速度還是太慢了。
最近,一個新的PR被合入:Make time-slicing opt-in[5]
這個PR中提到:在下個主版本中,會全量Concurrent Mode,但是這個Concurrent Mode會默認關閉時間切片功能。
就差直接喊話開發(fā)者:各位大爺們,求求你們快升級吧,OKR就指著他了😭
這種悲傷、殷切、又期待的心情直接導致了提交這次PR的Ricky小哥逐漸沙雕(狗頭保命):
React的操作系統(tǒng)夢,任重而道遠啊~~~
參考資料
[1]Initial Lanes implementation #18796:
https://github.com/facebook/react/pull/18796
[2]Inside the React Core team:
https://react.christmas/2020/24
[3]useMutableSource:
https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md
[4]will-this-react-global-state-work-in-concurrent-mode:
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode
[5]Make time-slicing opt-in:
https://github.com/facebook/react/pull/21072






















