React18,不遠啦?
在React前不久的一次PR #21488中,核心成員「Brian Vaughn」對React內(nèi)一些API、以及內(nèi)部flag作出調(diào)整。

其中最引人注目的改動是:React入口增加createRoot API。
業(yè)界將這一變化解讀為:Concurrent Mode(后文簡稱為CM)將在不久后穩(wěn)定,并出現(xiàn)在正式版中。

React17是一個過渡版本,用以穩(wěn)定CM。一旦CM穩(wěn)定,那v18的進度會大大加快。
可以說從18年到21年,React團隊的主要工作就是圍繞CM展開的,那么:
CM是什么?CM能解決React什么問題?為什么經(jīng)歷快4年,跨越16、17兩個版本,
CM還不穩(wěn)定?
本文將作出解答。
CM是什么
要了解CM(并發(fā)模式)是什么,首先需要知道React源碼的運行流程。
React大體可以分為兩個工作階段:
render階段
在render階段會計算一次更新中變化的部分(通過diff算法),因組件的render函數(shù)在該階段調(diào)用而得名。
render階段「可能」是異步的(取決于觸發(fā)更新的場景)。
commit階段
在commit階段會將render階段計算的需要變化的部分渲染在視圖中。對應ReactDOM來說會執(zhí)行appendChild、removeChild等。
commit階段一定是同步調(diào)用(這樣用戶不會看到渲染不完全的UI)
我們通過ReactDOM.render創(chuàng)建的應用屬于legacy模式。
在該模式下一次render階段對應一次commit階段。
如果我們通過ReactDOM.createRoot(當前穩(wěn)定版本中還沒有此API)創(chuàng)建的應用屬于開篇提到的CM(concurrent模式)
在CM下,更新有了優(yōu)先級的概念,render階段可能被高優(yōu)先級的更新打斷。
所以render階段可能會重復多次(被打斷后重新開始)。
可能多次render階段對應一次commit階段。
此外,還有個blocking模式用于方便開發(fā)者慢慢從legacy模式過渡到CM。
你可以從特性對比看到不同模式支持的特性:

為什么需要CM?
知道了CM是什么,那么他有什么用?為什么React核心團隊會耗時3年多(18年開始)來實現(xiàn)他?
這得從React的設計理念聊起。
我們可以從官網(wǎng)React哲學看到React的設計理念:
我們認為,
React是用JavaScript構(gòu)建「快速響應」的大型Web應用程序的首選方式。
其中「快速響應」是重點。
那么什么影響「快速響應」呢?React團隊給出的答案:
CPU的瓶頸和IO的瓶頸
CPU的瓶頸
考慮如下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階段變?yōu)?strong style="color: rgb(145, 109, 213);">「可中斷」的,
當瀏覽器一幀剩余時間不多時將控制權(quán)交給瀏覽器。等下一幀的空余時間再繼續(xù)組件render。
IO的瓶頸
除了長時間計算導致的卡頓,網(wǎng)絡請求時的loading狀態(tài)也會造成頁面不可交互,這就是IO的瓶頸。
IO瓶頸是客觀存在的。
作為前端,能做的只能是盡早請求需要的數(shù)據(jù)。
但是,通常情況下:「代碼可維護性」與「請求效率」是相悖的。
什么意思呢,舉個例子:
假設我們封裝了請求數(shù)據(jù)的方法useFetch,通過返回值是否存在區(qū)分是否請求到數(shù)據(jù)。
function App() {
const data = useFetch();
return {data ? <User data={data}/> : null};
}
為了提高「代碼可維護性」,useFetch與要渲染的組件User存在于同一個組件App中。
然而,如果User組件內(nèi)還需要進一步請求數(shù)據(jù)呢(如下profile數(shù)據(jù))?
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。
數(shù)據(jù)只能像瀑布的水一樣,一層一層流下來。
這種低效的請求數(shù)據(jù)方式被稱為waterfall。

為了提高「請求效率」,我們可以將“請求Profile組件所需數(shù)據(jù)的操作”提到App組件內(nèi),合并在useFetch中:
function App() {
const data = useFetch();
return {data ? <User data={data}/> : null};
}
但是這樣就降低了「代碼可維護性」(Profile組件離profile數(shù)據(jù)太遠)。
React團隊從Relay團隊借鑒經(jīng)驗,借助Suspense特性,提出了Server Components。
就是為了在處理IO瓶頸時兼顧「代碼可維護性」與「請求效率」。
這一特性的實現(xiàn)需要CM中「更新有不同優(yōu)先級」。
CM為什么花費這么久?
接下來,我們從源碼、特性、生態(tài)三個方面,自底向上看看CM的普及有多么不容易。
源碼層面
優(yōu)先級算法改造
在v16.13之前,React已經(jīng)實現(xiàn)了基本的CM功能。
我們之前聊過,CM有更新優(yōu)先級的概念。之前是通過一個毫秒數(shù)expirationTime標記「更新」的過期時間。
通過對比不同更新的
expirationTime判斷優(yōu)先級高低通過對比更新的
expirationTime與當前時間判斷更新是否過期(過期需要同步執(zhí)行)
但是,expirationTime作為一個與時間相關的浮點數(shù),無法表示「一批優(yōu)先級」這個概念。
為了實現(xiàn)更上層的Server Components特性,需要有「一批優(yōu)先級」這個概念。
于是,核心成員「Andrew Clark」開始了曠日持久的優(yōu)先級算法改造,見:PR lanes

Offscreen支持
在此同時,另一個成員「Luna Ruan」在開發(fā)一個新API —— Offscreen。
可以理解這是React版的Keep-Alive特性。

訂閱外部源
未開啟CM前,在一次更新如下三個生命周期只會調(diào)用一次:
componentWillMountcomponentWillReceivePropscomponentWillUpdate
但是開啟CM后,由于render階段可能被打斷、重復,所以他們可能被調(diào)用多次。
在訂閱外部源(比如注冊事件回調(diào))時,可能更新不及時或者內(nèi)存泄漏。
舉個例子:bindEvent是一個基于「發(fā)布訂閱」的外部依賴(比如一個原生DOM事件):
class App {
componentWillMount() {
bindEvent('eventA', data => {
thie.setState({data});
});
}
componentWillUnmount() {
bindEvent('eventA');
}
render() {
return <Card data={this.state.data}/>;
}
}
在componentWillMount中綁定,在componentWillUnmount中解綁。
當接收到事件后,更新data。
當render階段反復中斷、暫停后,有可能出現(xiàn):
事件最終綁定前(
bindEvent執(zhí)行前),事件源觸發(fā)了事件
此時App組件還未注冊該事件(bindEvent還未執(zhí)行),那么App獲取的data就是舊的。
為了解決這個潛在問題,核心成員「Brian Vaughn」開發(fā)了特性:create-subscription

用來在React中規(guī)范外部源的訂閱與更新。
簡單說就是將外部源的注冊與更新在commit階段與組件的狀態(tài)更新機制綁定上。
特性層面
當「源碼層面」的支持完備后,基于CM的新特性開發(fā)便提上日程。
這便是Suspense。
[Umbrella] Releasing Suspense #13206,這個PR負責記錄Suspense特性的進展。
Umbrella標記代表這個PR會影響非常多庫、組件、工具
可以看到,長長的時間線從18年一直到最近幾天。
最初Suspense只是「前端特性」,當時React SSR只能向前端傳遞「字符串」數(shù)據(jù)(也就是俗稱的脫水)
后來React實現(xiàn)了一套SSR時的組件「流式」傳輸協(xié)議,可以「流式」傳輸組件,而不僅僅是HTML字符串。
此時,Suspense被賦予更多職責。也擁有了更復雜的優(yōu)先級,這也是剛才講過的「優(yōu)先級算法改造」的一大原因。
最終的成果,就是今年早些時候推出的Server Components概念。
生態(tài)層面
當「源碼層面」支持了、「特性」也開發(fā)完成了,是不是就能無縫接入呢?
還早。
作為一艘行駛了8年的巨輪,React每次升級到最終社區(qū)普及,中間都有巨量的工作要做。
為了幫助社區(qū)慢慢過渡到CM,React做了如下工作:
開發(fā)
ScrictMode特性,并且是默認啟用的,規(guī)范開發(fā)者寫法將
componentWillXXX標記為unsafe,提醒用戶不要使用,未來會廢棄提出了新生命周期(
getDerivedStateFromProps、getSnapshotBeforeUpdate)替代如上將被廢棄的生命周期開發(fā)了
legacy模式與CM過渡的中間模式 ——blocking模式
而這,只是過渡過程中「最簡單」的部分。
難的部分是:
社區(qū)當前積累的大量基于
legacy模式的庫如何遷移?
很多動畫庫、狀態(tài)管理庫(比如mobX)的遷移并不簡單。
總結(jié)
我們介紹了CM的來龍去脈以及他遷移的難點。
通過這篇文章,想必你也知道了開頭那個為React增加createRoot(開啟CM的方法)是多么不容易。
好在一切都是值得的,如果說以前React的壁壘在于:開源時間早、社區(qū)規(guī)模大。
那么從CM開始,React 「可能」會是前端領域最復雜的視圖框架。
屆時,不會有任何一個React-like的框架能實現(xiàn)React同樣的feature。

但是也有人說,CM帶來的這些功能就是雞肋,我根本不需要。
你覺得CM怎么樣?歡迎留下你的討論。
