小推理:React18比老版React更優(yōu)秀的一個地方

大家好,我卡頌。
React18已經(jīng)進(jìn)入RC(release candidate)階段,距離正式版只有一步之遙了。
v18新增了很多特性,今天,我們不聊新特性,而是來講講v18相比老版更優(yōu)秀的一個細(xì)節(jié):
- v18中,組件render的次數(shù)可能更少。
狀態(tài)從何而來
在如下組件中:
function App() {
const [num, update] = useState(0);
// ...省略
App組件render后會執(zhí)行useState,返回num的最新值。
也就是說,組件必須render,才能知道最新的狀態(tài)。為什么會這樣呢?
考慮如下觸發(fā)更新的代碼:
const [num, update] = useState(0);
const onClick = () => {
update(100);
update(num + 1);
update(num => num * 3);
}
onClick執(zhí)行后觸發(fā)更新,更新導(dǎo)致App組件render,進(jìn)而useState執(zhí)行。
在useState內(nèi)部,會遵循如下流程計算num:
- update(100)將num變?yōu)?00。
- update(num + 1)將num變?yōu)?00 + 1 = 101。
- update(num => num * 3)將num變?yōu)?01 * 3 = 303。
即,App組件render時,num為303。
所以,狀態(tài)的計算需要先收集觸發(fā)的更新,再在useState中統(tǒng)一計算。
對于上述例子,將更新分別命名為u0~u2,則狀態(tài)的計算公式為:
baseState -> u0 -> u1 -> u2 = newState
Concurrent帶來的變化
Concurrent(并發(fā))為React帶來了「優(yōu)先級」的概念,反映到「狀態(tài)計算」上,根據(jù)觸發(fā)更新的場景,更新?lián)碛胁煌瑑?yōu)先級(比如onClick回調(diào)中觸發(fā)的更新優(yōu)先級高于useEffect回調(diào)中觸發(fā)的更新)。
表現(xiàn)在計算狀態(tài)中的區(qū)別就是,如果某個更新優(yōu)先級低,則會被跳過。
假設(shè)上述例子中u1優(yōu)先級低,那么App組件render時,計算num狀態(tài)的公式為:
// 其中u1因為優(yōu)先級低,被跳過
baseState -> u0 -> u2 = newState
即:
- update(100)將num變?yōu)?00。
- update(num => num * 3)將num變?yōu)?00 * 3 = 300。
顯然這個結(jié)果是不對的。
所以,并發(fā)情況下React計算狀態(tài)的邏輯會更復(fù)雜。具體來講,可能包含多輪計算。
當(dāng)計算狀態(tài)時,如果某次更新被跳過,則下次計算時會從被跳過的更新繼續(xù)往后計算。
比如上例中,u1被跳過。當(dāng)u1被跳過時,num為100。此時的狀態(tài)100,以及u1和「他后面的所有更新」都會保存下來,參與下次計算。
在例子中即為u1、u2保存下來。
下次更新的情況如下:
- 初始狀態(tài)為100,update(num + 1)將num變?yōu)?00 + 1 = 101。
- update(num => num * 3)將num變?yōu)?01 * 3 = 303。
可見,最終的結(jié)果303與「同步的React」是一致的,只是需要render兩次。
「同步的React」 render一次,結(jié)果為303。
「并發(fā)的React」 render兩次,結(jié)果分別為300(中間狀態(tài)),303(最終狀態(tài))。
新舊Concurrent的區(qū)別
從上例我們發(fā)現(xiàn),組件render的次數(shù)受「有多少更新被跳過」影響,實際可能不止render兩次,而是多次。
在老版「并發(fā)的React」中,表示「優(yōu)先級」的是一個被稱為expirationTime的時間戳。比較「更新是否應(yīng)該被跳過」的算法如下:
// 更新優(yōu)先級是否小于render的優(yōu)先級
if (updateExpirationTime < renderExpirationTime) {
// ...被跳過
} else {
// ...不跳過
}
在這種邏輯下,只要優(yōu)先級低,就會被跳過,就意味著多一次render。
在新版「并發(fā)的React」中,「優(yōu)先級」被保存在「31位的二進(jìn)制數(shù)」中。
舉個例子:
const renderLanes = 0b0101;
u1.lane = 0b0001;
u2.lane = 0b0010;
其中renderLanes是本次更新指定的「優(yōu)先級」。
比較「優(yōu)先級」的函數(shù)為:
function isSubsetOfLanes(set, subset) {
return (set & subset) === subset;
}
其中:
// true
isSubsetOfLanes(renderLanes, u1.lane)
// false
isSubsetOfLanes(renderLanes, u2.lane)
u1.lane包含于renderLanes中,代表這個更新?lián)碛凶銐騼?yōu)先級。
u2.lane不包含于renderLanes中,代表這個更新沒有足夠優(yōu)先級,被跳過。
但是「被跳過的更新」(例子中的u2)的lane會被重置為0,即:
u2.lane = 0b0000;
顯然任何lanes都是包含0的:
// true
isSubsetOfLanes(renderLanes, 0)
所以這個更新一定會在下次處理。換言之,在新版「并發(fā)的React」中,由于「優(yōu)先級原因被跳過」,導(dǎo)致的「重復(fù)render」,最多只會有2次。
總結(jié)
相比于老版「并發(fā)的React」,新版「并發(fā)的React」在render次數(shù)上會更有優(yōu)勢。
反映到用戶的感官上,用戶會更少看到「未計算完全的中間狀態(tài)」。






























