在React
前不久的一次PR #21488中,核心成員Brian Vaughn對React
內一些API
、以及內部flag
做出調整。html
其中最引人注目的改動是:React
入口增長createRoot API
。前端
業界將這一變化解讀爲:Concurrent Mode
(後文簡稱爲CM
)將在不久後穩定,並出如今正式版中。react
React17
是一個過渡版本,用以穩定CM
。一旦CM
穩定,那v18的進度會大大加快。git
能夠說從18年到21年,React
團隊的主要工做就是圍繞CM
展開的,那麼:github
CM
是什麼?CM
能解決React
什麼問題?CM
還不穩定?本文將做出解答。算法
要了解CM
(併發模式)是什麼,首先須要知道React
源碼的運行流程。瀏覽器
React
大致能夠分爲兩個工做階段:網絡
render
階段在render
階段會計算一次更新中變化的部分(經過diff算法),因組件的render
函數在該階段調用而得名。併發
render
階段可能是異步的(取決於觸發更新的場景)。app
commit
階段在commit
階段會將render
階段計算的須要變化的部分渲染在視圖中。對應ReactDOM
來講會執行appendChild
、removeChild
等。
commit
階段必定是同步調用(這樣用戶不會看到渲染不徹底的UI
)
咱們經過ReactDOM.render
建立的應用屬於legacy
模式。
在該模式下一次render
階段對應一次commit
階段。
若是咱們經過ReactDOM.createRoot
(當前穩定版本中尚未此API
)建立的應用屬於開篇提到的CM
(concurrent
模式)
在CM
下,更新有了優先級的概念,render
階段可能被高優先級的更新打斷。
因此render
階段可能會重複屢次(被打斷後從新開始)。
可能屢次render
階段對應一次commit
階段。
此外,還有個blocking
模式用於方便開發者慢慢從legacy
模式過渡到CM
。
你能夠從特性對比看到不一樣模式支持的特性:
知道了CM
是什麼,那麼他有什麼用?爲何React
核心團隊會耗時3年多(18年開始)來實現他?
這得從React
的設計理念聊起。
咱們能夠從官網React哲學看到React
的設計理念:
咱們認爲,React
是用JavaScript
構建 快速響應的大型Web
應用程序的首選方式。
其中快速響應是重點。
那麼什麼影響快速響應呢?React
團隊給出的答案:
CPU
的瓶頸和IO
的瓶頸
考慮以下demo
,咱們渲染3000的列表項:
function App() { const len = 3000; return ( <ul> {Array(len).fill(0).map((_, i) => <li>{i}</li>)} </ul> ); } const rootEl = document.querySelector("#root"); ReactDOM.render(<App/>, rootEl);
剛纔說過,在legacy
模式下render
階段不會被打斷,則這3000個li
的render
都得在同一個瀏覽器宏任務中完成。
長時間的計算會阻塞線程,形成頁面掉幀,這就是CPU
的瓶頸。
解決的辦法就是:啓用CM
,將render
階段變爲可中斷的,
當瀏覽器一幀剩餘時間很少時將控制權交給瀏覽器。等下一幀的空餘時間再繼續組件render
。
除了長時間計算致使的卡頓,網絡請求時的loading
狀態也會形成頁面不可交互,這就是IO
的瓶頸。
IO
瓶頸是客觀存在的。
做爲前端,能作的只能是儘早請求須要的數據。
可是,一般狀況下:代碼可維護性與請求效率是相悖的。
什麼意思呢,舉個例子:
假設咱們封裝了請求數據的方法useFetch
,經過返回值是否存在區分是否請求到數據。
function App() { const data = useFetch(); return {data ? <User data={data}/> : null}; }
爲了提升代碼可維護性,useFetch
與要渲染的組件User
存在於同一個組件App
中。
然而,若是User
組件內還須要進一步請求數據呢(以下profile
數據)?
function User({data}) { const {id, name} = data?.id || {}; const profile = useFetch(id); return ( <div> <p>{name}</p> {profile ? <Profile data={profile} /> : null} </div> ) }
本着代碼可維護性原則,useFetch
與要渲染的組件Profile
存在於同一個組件User
中。
可是,這樣組織代碼,Profile
組件只能等User
render
後再render
。
數據只能像瀑布的水同樣,一層一層流下來。
這種低效的請求數據方式被稱爲waterfall
。
爲了提升請求效率,咱們能夠將「請求Profile
組件所需數據的操做」提到App
組件內,合併在useFetch
中:
function App() { const data = useFetch(); return {data ? <User data={data}/> : null}; }
可是這樣就下降了代碼可維護性(Profile
組件離profile
數據太遠)。
React
團隊從Relay
團隊借鑑經驗,藉助Suspense
特性,提出了Server Components。
就是爲了在處理IO
瓶頸時兼顧代碼可維護性與請求效率。
這一特性的實現須要CM
中更新有不一樣優先級。
接下來,咱們從源碼
、特性
、生態
三個方面,自底向上看看CM
的普及有多麼不容易。
在v16.13以前,React
已經實現了基本的CM
功能。
咱們以前聊過,CM
有更新優先級的概念。以前是經過一個毫秒數expirationTime
標記更新的過時時間。
expirationTime
判斷優先級高低expirationTime
與當前時間判斷更新是否過時(過時須要同步執行)可是,expirationTime
做爲一個與時間相關的浮點數,沒法表示一批優先級這個概念。
爲了實現更上層的Server Components
特性,須要有一批優先級這個概念。
因而,核心成員Andrew Clark開始了曠日持久的優先級算法改造,見:PR lanes
在此同時,另外一個成員Luna Ruan在開發一個新API
—— Offscreen
。
能夠理解這是React
版的Keep-Alive
特性。
未開啓CM
前,在一次更新以下三個生命週期只會調用一次:
componentWillMount
componentWillReceiveProps
componentWillUpdate
可是開啓CM
後,因爲render
階段可能被打斷、重複,因此他們可能被調用屢次。
在訂閱外部源(好比註冊事件回調)時,可能更新不及時或者內存泄漏。
舉個例子:bindEvent
是一個基於發佈訂閱的外部依賴(好比一個原生DOM
事件):
class App { componentWillMount() { bindEvent('eventA', data => { thie.setState({data}); }); } componentWillUnmount() { bindEvent('eventA'); } render() { return <Card data={this.state.data}/>; } }
在componentWillMount
中綁定,在componentWillUnmount
中解綁。
當接收到事件後,更新data
。
當render
階段反覆中斷、暫停後,有可能出現:
事件最終綁定前(
bindEvent
執行前),事件源觸發了事件
此時App
組件還未註冊該事件(bindEvent
還未執行),那麼App
獲取的data
就是舊的。
爲了解決這個潛在問題,核心成員Brian Vaughn開發了特性:create-subscription
用來在React
中規範外部源的訂閱與更新。
簡單說就是將外部源的註冊與更新在commit
階段與組件的狀態更新機制綁定上。
當源碼層面的支持完備後,基於CM
的新特性開發便提上日程。
這即是Suspense
。
[[Umbrella] Releasing Suspense #13206](https://github.com/facebook/r...PR
負責記錄Suspense
特性的進展。
Umbrella
標記表明這個PR
會影響很是多庫、組件、工具
能夠看到,長長的時間線從18年一直到最近幾天。
最初Suspense
只是前端特性,當時React SSR
只能向前端傳遞字符串數據(也就是俗稱的脫水
)
後來React
實現了一套SSR
時的組件流式傳輸協議,能夠流式傳輸組件,而不只僅是HTML
字符串。
此時,Suspense
被賦予更多職責。也擁有了更復雜的優先級,這也是剛纔講過的優先級算法改造的一大緣由。
最終的成果,就是今年早些時候推出的Server Components
概念。
當源碼層面支持了、特性也開發完成了,是否是就能無縫接入呢?
還早。
做爲一艘行駛了8年的巨輪,React
每次升級到最終社區普及,中間都有巨量的工做要作。
爲了幫助社區慢慢過渡到CM
,React
作了以下工做:
ScrictMode
特性,而且是默認啓用的,規範開發者寫法componentWillXXX
標記爲unsafe
,提醒用戶不要使用,將來會廢棄getDerivedStateFromProps
、getSnapshotBeforeUpdate
)替代如上將被廢棄的生命週期legacy
模式與CM
過渡的中間模式 —— blocking
模式而這,只是過渡過程當中最簡單的部分。
難的部分是:
社區當前積累的大量基於
legacy
模式的庫如何遷移?
不少動畫庫、狀態管理庫(好比mobX
)的遷移並不簡單。
咱們介紹了CM
的前因後果以及他遷移的難點。
經過這篇文章,想必你也知道了開頭那個爲React
增長createRoot
(開啓CM
的方法)是多麼不容易。
好在一切都是值得的,若是說之前React
的壁壘在於:開源時間早、社區規模大。
那麼從CM
開始,React
可能會是前端領域最複雜的視圖框架。
屆時,不會有任何一個React-like
的框架能實現React
一樣的feature
。
可是也有人說,CM
帶來的這些功能就是雞肋,我根本不須要。
你以爲CM
怎麼樣?歡迎留下你的討論。