React18,不遠(yuǎn)啦?
React前不久的一次PR #21488中,核心成員「Brian Vaughn」對(duì)React內(nèi)一些API、以及內(nèi)部flag作出調(diào)整。
其中最引人注目的改動(dòng)是:React入口增加createRoot API。
業(yè)界將這一變化解讀為:Concurrent Mode(后文簡(jiǎn)稱為CM)將在不久后穩(wěn)定,并出現(xiàn)在正式版中。

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

為什么需要CM?
知道了CM是什么,那么他有什么用?為什么React核心團(tuán)隊(duì)會(huì)耗時(shí)3年多(18年開始)來實(shí)現(xiàn)他?
這得從React的設(shè)計(jì)理念聊起。
我們可以從官網(wǎng)React哲學(xué)看到React的設(shè)計(jì)理念:
我們認(rèn)為,
React是用JavaScript構(gòu)建「快速響應(yīng)」的大型Web應(yīng)用程序的首選方式。
其中「快速響應(yīng)」是重點(diǎn)。
那么什么影響「快速響應(yīng)」呢?React團(tuán)隊(duì)給出的答案:
CPU的瓶頸和IO的瓶頸
CPU的瓶頸
考慮如下demo,我們渲染3000的列表項(xiàng):
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階段不會(huì)被打斷,則這3000個(gè)li的render都得在同一個(gè)瀏覽器宏任務(wù)中完成。
長時(shí)間的計(jì)算會(huì)阻塞線程,造成頁面掉幀,這就是CPU的瓶頸。
解決的辦法就是:?jiǎn)⒂?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">CM,將render階段變?yōu)?strong style="color: rgb(145, 109, 213);">「可中斷」的,
當(dāng)瀏覽器一幀剩余時(shí)間不多時(shí)將控制權(quán)交給瀏覽器。等下一幀的空余時(shí)間再繼續(xù)組件render。
IO的瓶頸
除了長時(shí)間計(jì)算導(dǎo)致的卡頓,網(wǎng)絡(luò)請(qǐng)求時(shí)的loading狀態(tài)也會(huì)造成頁面不可交互,這就是IO的瓶頸。
IO瓶頸是客觀存在的。
作為前端,能做的只能是盡早請(qǐng)求需要的數(shù)據(jù)。
但是,通常情況下:「代碼可維護(hù)性」與「請(qǐng)求效率」是相悖的。
什么意思呢,舉個(gè)例子:
假設(shè)我們封裝了請(qǐng)求數(shù)據(jù)的方法useFetch,通過返回值是否存在區(qū)分是否請(qǐng)求到數(shù)據(jù)。
function App() {
const data = useFetch();
return {data ? <User data={data}/> : null};
}
為了提高「代碼可維護(hù)性」,useFetch與要渲染的組件User存在于同一個(gè)組件App中。
然而,如果User組件內(nèi)還需要進(jìn)一步請(qǐng)求數(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>
)
}
本著「代碼可維護(hù)性」原則,useFetch與要渲染的組件Profile存在于同一個(gè)組件User中。
但是,這樣組織代碼,Profile組件只能等User render后再render。
數(shù)據(jù)只能像瀑布的水一樣,一層一層流下來。
這種低效的請(qǐng)求數(shù)據(jù)方式被稱為waterfall。

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

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

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

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

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