React 18 全覽
在 2021 年 6 月 8 號(hào),React 公布了 v18 版本的發(fā)布計(jì)劃,并發(fā)布了 alpha 版本。經(jīng)過將近一年的發(fā)布前準(zhǔn)備,在 2022 年 3 月 29 日,React 18 正式版終于和大家見面了。
React 18 應(yīng)該是最近幾年的一個(gè)重磅版本,React 官方對(duì)它寄予了厚望。不然也不會(huì)將 React 17 作為一個(gè)過渡版本,也不會(huì)光發(fā)布準(zhǔn)備工作就做了一年。
在過去一年,我們已經(jīng)或多或少了解到一些 React 18 的新功能。這篇文章我會(huì)通過豐富的示例,向大家系統(tǒng)的介紹 React 18 帶來的改變。當(dāng)然本文融入了很多個(gè)人理解,如有不對(duì),煩請(qǐng)指正。
Concurrent Mode
Concurrent Mode(以下簡(jiǎn)稱 CM)翻譯叫并發(fā)模式,這個(gè)概念我已經(jīng)聽了好多年了,并且一度非常擔(dān)憂
React 官方憋了好多年的大招,會(huì)不會(huì)是一個(gè)破壞性不兼容的超級(jí)大版本?就像 VUE v3 和 v2。 現(xiàn)有的生態(tài)是不是都得跟著大版本升級(jí)?比如 ant design,ahooks 等。
隨著對(duì) CM 的了解,我發(fā)現(xiàn)它其實(shí)是人畜無害的。
CM 本身并不是一個(gè)功能,而是一個(gè)底層設(shè)計(jì),它使 React 能夠同時(shí)準(zhǔn)備多個(gè)版本的 UI。

在以前,React 在狀態(tài)變更后,會(huì)開始準(zhǔn)備虛擬 DOM,然后渲染真實(shí) DOM,整個(gè)流程是串行的。一旦開始觸發(fā)更新,只能等流程完全結(jié)束,期間是無法中斷的。

在 CM 模式下,React 在執(zhí)行過程中,每執(zhí)行一個(gè) Fiber,都會(huì)看看有沒有更高優(yōu)先級(jí)的更新,如果有,則當(dāng)前低優(yōu)先級(jí)的的更新會(huì)被暫停,待高優(yōu)先級(jí)任務(wù)執(zhí)行完之后,再繼續(xù)執(zhí)行或重新執(zhí)行。
CM 模式有點(diǎn)類似計(jì)算機(jī)的多任務(wù)處理,處理器在同時(shí)進(jìn)行的應(yīng)用程序之間快速切換,也許 React 應(yīng)該改名叫 ReactOS 了。
這里舉個(gè)例子:我們正在看電影,這時(shí)候門鈴響了,我們要去開門拿快遞。在 React 18 以前,一旦我們開始看電影,就不能被終止,必須等電影看完之后,才會(huì)去開門。而在 React 18 CM 模式之后,我們就可以暫停電影,等開門拿完快遞之后,再重新繼續(xù)看電影。
不過對(duì)于普通開發(fā)者來說,我們一般是不會(huì)感知到 CM 的存在的,在升級(jí)到 React 18 之后,我們的項(xiàng)目不會(huì)有任何變化。
我們需要關(guān)注的是基于 CM 實(shí)現(xiàn)的上層功能,比如 Suspense、Transitions、streaming server rendering(流式服務(wù)端渲染), 等等。
React 18 的大部分功能都是基于 CM 架構(gòu)實(shí)現(xiàn)出來的,并且這這是一個(gè)開始,未來會(huì)有更多基于 CM 實(shí)現(xiàn)的高級(jí)能力。
startTransition
我們?nèi)绻鲃?dòng)發(fā)揮 CM 的優(yōu)勢(shì),那就離不開 startTransition。
React 的狀態(tài)更新可以分為兩類:
緊急更新(Urgent updates):比如打字、點(diǎn)擊、拖動(dòng)等,需要立即響應(yīng)的行為,如果不立即響應(yīng)會(huì)給人很卡,或者出問題了的感覺 過渡更新(Transition updates):將 UI 從一個(gè)視圖過渡到另一個(gè)視圖。不需要即時(shí)響應(yīng),有些延遲是可以接受的。
我以前會(huì)認(rèn)為,CM 模式會(huì)自動(dòng)幫我們區(qū)分不同優(yōu)先級(jí)的更新,一鍵無憂享受。很遺憾的是,CM 只是提供了可中斷的能力,默認(rèn)情況下,所有的更新都是緊急更新。
這是因?yàn)?React 并不能自動(dòng)識(shí)別哪些更新是優(yōu)先級(jí)更高的。
const?[inputValue,?setInputValue]?=?useState();
const?onChange?=?(e)=>{
??setInputValue(e.target.value);
??//?更新搜索列表
??setSearchQuery(e.target.value);
}
return?(
??<input?value={inputValue}?onChange={onChange}?/>
)
比如以上示例,用戶的鍵盤輸入操作后,setInputValue會(huì)立即更新用戶的輸入到界面上,是緊急更新。而setSearchQuery是根據(jù)用戶輸入,查詢相應(yīng)的內(nèi)容,是非緊急的。
但是 React 確實(shí)沒有能力自動(dòng)識(shí)別。所以它提供了 startTransition讓我們手動(dòng)指定哪些更新是緊急的,哪些是非緊急的。
//?緊急的
setInputValue(e.target.value);
startTransition(()?=>?{
??setSearchQuery(input);?//?非緊急的
});
如上代碼,我們通過 startTransition來標(biāo)記一個(gè)非緊急更新,讓該狀態(tài)觸發(fā)的變更變成低優(yōu)先級(jí)的。
光用文字描述大家可能沒有體驗(yàn),接下來我們通過一個(gè)示例來認(rèn)識(shí)下可中斷渲染對(duì)性能的爆炸提升。
示例頁面:https://react-fractals-git-react-18-swizec.vercel.app/[1]
如下圖,我們需要畫一個(gè)畢達(dá)哥拉斯樹,通過一個(gè) Slider 來控制樹的傾斜。
那我們的代碼會(huì)很簡(jiǎn)單,如下所示,我們只需要一個(gè) treeLeanstate 來管理狀態(tài)。
const?[treeLean,?setTreeLean]?=?useState(0)
function?changeTreeLean(event)?{
??const?value?=?Number(event.target.value);
??setTreeLean(value);
}
return?(
??<>
????<input?type="range"?value={treeLean}?onChange={changeTreeLean}?/>
????<Pythagoras?lean={treeLean}?/>
??>
)
在每次 Slider 拖動(dòng)后,React 執(zhí)行流程大致如下:
更新 treeLean 渲染 input,填充新的 value 重新渲染樹組件 Pythagoras
每一次用戶拖動(dòng) Slider,都會(huì)同步執(zhí)行上述三步。但當(dāng)樹的節(jié)點(diǎn)足夠多的時(shí)候,Pythagoras 渲染一次就非常慢,就會(huì)導(dǎo)致 Slider 的 value 回填變慢,用戶感覺到嚴(yán)重的卡頓。如下圖。
當(dāng)數(shù)的節(jié)點(diǎn)足夠大時(shí),已經(jīng)卡到爆炸了。在 React 18 以前,我們是沒有什么好的辦法來解決這個(gè)問題的。但基于 React 18 CM 的可中斷渲染機(jī)制,我們可以將樹的更新渲染標(biāo)記為低優(yōu)先級(jí)的,就不會(huì)感覺到卡頓了。

const?[treeLeanInput,?setTreeLeanInput]?=?useState(0);
const?[treeLean,?setTreeLean]?=?useState(0);
function?changeTreeLean(event)?{
??const?value?=?Number(event.target.value);
??setTreeLeanInput(value)
??//?將?treeLean?的更新用?startTransition?包裹
??React.startTransition(()?=>?{
????setTreeLean(value);
??});
}
return?(
??<>
????<input?type="range"?value={treeLeanInput}?onChange={changeTreeLean}?/>
????<Pythagoras?lean={treeLean}?/>
??>
)
以上代碼,我們通過 startTransition 標(biāo)記了非緊急更新,讓樹的更新變成低優(yōu)先級(jí)的,可以被隨時(shí)中止,保證了高優(yōu)先級(jí)的 Slider 的體驗(yàn)。
此時(shí)更新流程變?yōu)榱?/p>
input 更新 treeLeanInput 狀態(tài)變更 準(zhǔn)備新的 DOM 渲染 DOM 樹更新(這一次更新是低優(yōu)先級(jí)的,隨時(shí)可以被中止) treeLean 狀態(tài)變更 準(zhǔn)備新的 DOM 渲染 DOM
React 會(huì)在高優(yōu)先級(jí)更新渲染完成之后,才會(huì)啟動(dòng)低優(yōu)先級(jí)更新渲染,并且低優(yōu)先級(jí)渲染隨時(shí)可被其它高優(yōu)先級(jí)更新中斷。
當(dāng)然,在低優(yōu)先狀態(tài)等待更新過程中,如果能有一個(gè) Loading 狀態(tài),那就更好了。React 18 提供了 useTransition來跟蹤 transition 狀態(tài)。
const?[treeLeanInput,?setTreeLeanInput]?=?useState(0);
const?[treeLean,?setTreeLean]?=?useState(0);
//?實(shí)時(shí)監(jiān)聽?transition?狀態(tài)
const?[isPending,?startTransition]?=?useTransition();
function?changeTreeLean(event)?{
??const?value?=?Number(event.target.value);
??setTreeLeanInput(value)
??React.startTransition(()?=>?{
????setTreeLean(value);
??});
}
return?(
??<>
????<input?type="range"?value={treeLeanInput}?onChange={changeTreeLean}?/>
????<Spin?spinning={isPending}>
??????<Pythagoras?lean={treeLean}?/>
????Spin>
??>
)
自動(dòng)批處理 Automatic Batching
批處理是指 React 將多個(gè)狀態(tài)更新,聚合到一次 render 中執(zhí)行,以提升性能。比如
function?handleClick()?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
??//?React?只會(huì)?re-render?一次,這就是批處理
}
在 React 18 之前,React 只會(huì)在事件回調(diào)中使用批處理,而在 Promise、setTimeout、原生事件等場(chǎng)景下,是不能使用批處理的。
setTimeout(()?=>?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
??//?React?會(huì)?render?兩次,每次?state?變化更新一次
},?1000);
而在 React 18 中,所有的狀態(tài)更新,都會(huì)自動(dòng)使用批處理,不關(guān)心場(chǎng)景。
function?handleClick()?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
??//?React?只會(huì)?re-render?一次,這就是批處理
}
setTimeout(()?=>?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
??//?React?只會(huì)?re-render?一次,這就是批處理
},?1000);
如果你在某種場(chǎng)景下不想使用批處理,你可以通過 flushSync來強(qiáng)制同步執(zhí)行(比如:你需要在狀態(tài)更新后,立刻讀取新 DOM 上的數(shù)據(jù)等。)
import?{?flushSync?}?from?'react-dom';
function?handleClick()?{
??flushSync(()?=>?{
????setCounter(c?=>?c?+?1);
??});
??//?React?更新一次?DOM
??flushSync(()?=>?{
????setFlag(f?=>?!f);
??});
??//?React?更新一次?DOM
}
React 18 的批處理在絕大部分場(chǎng)景下是沒有影響,但在 Class 組件中,如果你在兩次 setState 中間讀取了 state 值,會(huì)出現(xiàn)不兼容的情況,如下示例。
handleClick?=?()?=>?{
??setTimeout(()?=>?{
????this.setState(({?count?})?=>?({?count:?count?+?1?}));
????//?在?React17?及之前,打印出來是?{?count:?1,?flag:?false?}
????//?在?React18,打印出來是?{?count:?0,?flag:?false?}
????console.log(this.state);
????this.setState(({?flag?})?=>?({?flag:?!flag?}));
??});
};
當(dāng)然你可以通過 flushSync來修正它。
handleClick?=?()?=>?{
??setTimeout(()?=>?{
????ReactDOM.flushSync(()?=>?{
??????this.setState(({?count?})?=>?({?count:?count?+?1?}));
????});
????//?在?React18,打印出來是?{?count:?1,?flag:?false?}
????console.log(this.state);
????this.setState(({?flag?})?=>?({?flag:?!flag?}));
??});
};
流式 SSR
SSR 一次頁面渲染的流程大概為:
服務(wù)器 fetch 頁面所需數(shù)據(jù) 數(shù)據(jù)準(zhǔn)備好之后,將組件渲染成 string 形式作為 response 返回 客戶端加載資源 客戶端合成(hydrate)最終的頁面內(nèi)容
在傳統(tǒng)的 SSR 模式中,上述流程是串行執(zhí)行的,如果其中有一步比較慢,都會(huì)影響整體的渲染速度。
而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允許服務(wù)端一點(diǎn)一點(diǎn)的返回頁面。
假設(shè)我們有一個(gè)頁面,包含了 NavBar、Sidebar、Post、Comments 等幾個(gè)部分,在傳統(tǒng)的 SSR 模式下,我們必須請(qǐng)求到 Post 數(shù)據(jù),請(qǐng)求到 Comments 數(shù)據(jù)后,才能返回完整的 HTML。
??<nav>
????
????<a?href="/">Homea>
???nav>
??<aside>
????
????<a?href="/profile">Profilea>
??aside>
??<article>
????
????<p>Hello?worldp>
??article>
??<section>
????
????<p>First?commentp>
????<p>Second?commentp>
??section>
</main>
但如果 Comments 數(shù)據(jù)請(qǐng)求很慢,會(huì)拖慢整個(gè)流程。
在 React 18 中,我們通過 Suspense包裹,可以告訴 React,我們不需要等這個(gè)組件,可以先返回其它內(nèi)容,等這個(gè)組件準(zhǔn)備好之后,單獨(dú)返回。
??<NavBar?/>
??<Sidebar?/>
??<RightPane>
????<Post?/>
????<Suspense?fallback={<Spinner?/>}>
??????<Comments?/>
????Suspense>
??RightPane>
</Layout>
如上,我們通過 Suspense包裹了 Comments 組件,那服務(wù)器首次返回的 HTML 是下面這樣的,組件處通過 loading進(jìn)行了占位。
??<nav>
????
????<a?href="/">Homea>
???nav>
??<aside>
????
????<a?href="/profile">Profilea>
??aside>
??<article>
????
????<p>Hello?worldp>
??article>
??<section?id="comments-spinner">
????
????<img?width=400?src="spinner.gif"?alt="Loading..."?/>
??section>
</main>
當(dāng) 組件準(zhǔn)備好之后,React 會(huì)通過同一個(gè)流(stream)發(fā)送給瀏覽器(res.send 替換成 res.socket),并替換到相應(yīng)位置。
