React18正式版源碼級(jí)剖析
本文適合對(duì)React18.0.0源碼感興趣的小伙伴閱讀。
歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
一、前言
本文是廣東靚仔的好友bubucuo-高老師寫的,高老師最近在準(zhǔn)備React18的視頻,有興趣的小伙伴可以去學(xué)習(xí)學(xué)習(xí)。
React18最重要的改變必須是Concurrent,就像哪吒降生一樣,打磨了很長時(shí)間了,終于正式見人了。

Concurrent Or Concurrency,中文我們通常翻譯為并發(fā),也有少部分翻譯成并行。React已經(jīng)著手開發(fā)Concurrent幾年了,但是一直只存在于實(shí)驗(yàn)版本。到了React18,Concurrent終于正式投入使用了。
Concurrent并不是API之類的特性,而是一種能讓你的React項(xiàng)目同時(shí)具有多個(gè)版本UI的幕后機(jī)制,相當(dāng)愛迪生背后的特斯拉。
Concurrent很重要,雖然它不是API之類的新特性,但是如果你想解鎖React18的大部分新特性,諸如transition、Suspense等,背后就要依賴Concurrent這位大佬。

是的,如果你不想追求high level,就別學(xué)了。
二、Concurrent
什么是Concurrent
Concurrent最主要的特點(diǎn)就是渲染是可中斷的。沒錯(cuò),以前是不可中斷的,也就是說,以前React中的update是同步渲染,在這種情況下,一旦update開啟,在任務(wù)完成前,都不可中斷。
注意:這里說的同步,和setState所謂的同步異步不是一碼事,而且setState所謂的異步本質(zhì)上是個(gè)批量處理。
Concurrent模式特點(diǎn)
在Concurrent模式下,update開始了也可以中斷,晚點(diǎn)再繼續(xù)嘛,當(dāng)然中間也可能被遺棄掉。
關(guān)于可中斷
先說可中斷這件事情的重要性。對(duì)于React來說,任務(wù)可能很多,如果不區(qū)分優(yōu)先級(jí),那就是先來后到的順序。雖然聽起來很合理,但是現(xiàn)實(shí)是普通車輛就應(yīng)該給救護(hù)車讓路,因?yàn)槭掠休p重緩急嘛。那么在React中呢,如果高優(yōu)先級(jí)任務(wù)來了,但是低優(yōu)先級(jí)任務(wù)還沒有處理完畢,就會(huì)造成高優(yōu)先級(jí)任務(wù)等待的局面。比如說,某個(gè)低優(yōu)先級(jí)任務(wù)還在緩慢中,input框忽然被用戶觸發(fā),但是由于主線程被占著,沒有人搭理用戶,結(jié)果是用戶哐哐輸入,但是input沒有任何反應(yīng)。用戶一怒之下就走了,那你那個(gè)低優(yōu)先級(jí)的任務(wù)還更新個(gè)什么呢,用戶都沒了。
由此可見,對(duì)于復(fù)雜項(xiàng)目來說,任務(wù)可中斷這件事情很重要。那么問題來了,React是如何做到的呢,其實(shí)基礎(chǔ)還是fiber,fiber本身鏈表結(jié)構(gòu),就是指針嘛,想指向別的地方加個(gè)屬性值就行了。
關(guān)于被遺棄
在Concurrent模式下,有些update可能會(huì)被遺棄掉。先舉個(gè)??:比如說,我看電視的時(shí)候,切換遙控器,從1頻道切換到2頻道,再切換到3頻道,最后在4頻道停下來。假如這些頻道都是UI,那么2、3頻道的渲染其實(shí)我并不關(guān)心,我只關(guān)心4頻道的結(jié)果,如果你非要花時(shí)間把2和3頻道的UI也渲染出來,最終導(dǎo)致4頻道很久之后才渲染出來,那我肯定不開心。正確的做法應(yīng)該是盡快渲染4頻道就行了,至于2和3頻道,不管渲染了多少了,遺棄了就行了,反正也不需要了。
最后回到項(xiàng)目的實(shí)際場景,比如我想在淘寶搜索“老人與?!保敲次以谳斎肟蜉斎搿袄先伺c?!钡倪^程中,“老人”會(huì)有對(duì)應(yīng)的模糊查詢結(jié)果,但是不一定是我想要的結(jié)果,所以這個(gè)時(shí)候的模糊查詢框的update就是低優(yōu)先級(jí),“老人”對(duì)應(yīng)UI的update相對(duì)input的update,優(yōu)先級(jí)就會(huì)低一些。在現(xiàn)在React18中,這個(gè)模糊查詢相關(guān)的UI可以被當(dāng)做transition。關(guān)于transition,等下我會(huì)有細(xì)講。

關(guān)于狀態(tài)復(fù)用
Concurrent模式下,還支持狀態(tài)的復(fù)用。某些情況下,比如用戶走了,又回來,那么上一次的頁面狀態(tài)應(yīng)當(dāng)被保存下來,而不是完全從頭再來。當(dāng)然實(shí)際情況下不能緩存所有的頁面,不然內(nèi)存不得爆炸,所以還得做成可選的。目前,React正在用Offscreen組件來實(shí)現(xiàn)這個(gè)功能。嗯,也就是這關(guān)于這個(gè)狀態(tài)復(fù)用,其實(shí)還沒完成呢。不過源碼中已經(jīng)在做了:

另外,使用OffScreen,除了可以復(fù)用原先的狀態(tài),我們也可以使用它來當(dāng)做新UI的緩存準(zhǔn)備,就是雖然新UI還沒登場,但是可以先在后臺(tái)準(zhǔn)備著嘛,這樣一旦輪到它,就可以立馬快速地渲染出來。
Concurrent總結(jié)
總結(jié)一下,Concurrent并不是API之類的新特性,但是呢,它很重要,因?yàn)樗荝eact18大部分新特性的實(shí)現(xiàn)基礎(chǔ),包括Suspense、transitions、流式服務(wù)端渲染等。三、React的新特性
前文說了那么多Concurrent并不是新特性,而是React18新特性的實(shí)現(xiàn)基礎(chǔ)。那么新特性都有哪些呢,下面來看吧:
react-dom/client中的createRoot
創(chuàng)建一個(gè)初次渲染或者更新,以前我們用的是ReactDOM.render,現(xiàn)在改用react-dom/client中的createRoot,這個(gè)函數(shù)的返回值是卸載函數(shù)。
ssr中的ReactDOM.hydrate也換成了新的hydrateRoot。
以上兩個(gè)API目前依然支持,只是已經(jīng)移入legacy模式,開發(fā)環(huán)境下會(huì)報(bào)warning。
自動(dòng)批量處理 Automatic Batching
如果你是React技術(shù)棧,那么你一定遇到過無數(shù)次這樣的面試題:

先回答上面那個(gè)問題,可同步可異步,同步的話把setState放在promises、setTimeout或者原生事件中等。所謂異步就是個(gè)批量處理,為什么要批量處理呢。舉個(gè)例子,老人以打漁為生,難道要每打到一條沙丁魚就下船去集市上賣掉嗎,那跑來跑去的成本太高了,賣魚的錢都不夠路費(fèi)的。所以老人都是打到魚之后先放到船艙,一段時(shí)間之后再跑一次集市,批量賣掉那些魚。對(duì)于React來說,也是這樣,state攢夠了再一起更新嘛。
//?以前:?這里的兩次setState并沒有批量處理,React會(huì)render兩次
setTimeout(()?=>?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
},?1000);
//?React18:?自動(dòng)批量處理,這里只會(huì)render一次
setTimeout(()?=>?{
??setCount(c?=>?c?+?1);
??setFlag(f?=>?!f);
},?1000);
所以如果你項(xiàng)目中還在用setTimeout之列的“黑科技”實(shí)現(xiàn)setState的同步的話,升級(jí)React18之前,記得改一下~???//?import?{?flushSync?}?from?"react-dom";
???changeCount?=?()?=>?{
????const?{?count?}?=?this.state;
????flushSync(()?=>?{
??????this.setState({
????????count:?count?+?1,
??????});
????});
????console.log("改變count",?this.state.count);?//sy-log
??};
??
??//?transition
React把update分成兩種:
Urgent updates?緊急更新,指直接交互,通常指的用戶交互。如點(diǎn)擊、輸入等。這種更新一旦不及時(shí),用戶就會(huì)覺得哪里不對(duì)。
Transition updates?過渡更新,如UI從一個(gè)視圖向另一個(gè)視圖的更新。通常這種更新用戶并不著急看到。
startTransition
startTransition可以用在任何你想更新的時(shí)候。但是從實(shí)際來說,以下是兩種典型適用場景:
渲染慢:如果你有很多沒那么著急的內(nèi)容要渲染更新。
網(wǎng)絡(luò)慢:如果你的更新需要花較多時(shí)間從服務(wù)端獲取。這個(gè)時(shí)候也可以再結(jié)合
Suspense。
import?{useEffect,?useState,?Suspense}?from?"react";
import?Button?from?"../components/Button";
import?User?from?"../components/User";
import?Num?from?"../components/Num";
import?{fetchData}?from?"../utils";
const?initialResource?=?fetchData();
export?default?function?TransitionPage(props)?{
??const?[resource,?setResource]?=?useState(initialResource);
??//?useEffect(()?=>?{
??//???console.log("resource",?resource);?//sy-log
??//?},?[resource]);
??return?(
????<div>
??????<h3>TransitionPageh3>
??????<Suspense?fallback={<h1>loading?-?userh1>}>
????????<User?resource={resource}?/>
??????Suspense>
??????<Suspense?fallback={<h1>loading-numh1>}>
????????<Num?resource={resource}?/>
??????Suspense>
??????<Button
????????refresh={()?=>?{
??????????setResource(fetchData());
????????}}
??????/>
????div>
??);
}
Button
import?{
??//startTransition,
??useTransition,
}?from?"react";
export?default?function?Button({refresh})?{
??const?[isPending,?startTransition]?=?useTransition();
??return?(
????<div?className="border">
??????<h3>Buttonh3>
??????<button
????????onClick={()?=>?{
??????????startTransition(()?=>?{
????????????refresh();
??????????});
????????}}
????????disabled={isPending}>
????????點(diǎn)擊刷新數(shù)據(jù)
??????button>
??????{isPending???<div>loading...div>?:?null}
????div>
??);
}
與setTimeout異同
在startTransition出現(xiàn)之前,我們可以使用setTimeout來實(shí)現(xiàn)優(yōu)化。但是現(xiàn)在在處理上面的優(yōu)化的時(shí)候,有了startTransition基本上可以拋棄setTimeout了,原因主要有以三點(diǎn):首先,與setTimeout不同的是,startTransition并不會(huì)延遲調(diào)度,而是會(huì)立即執(zhí)行,startTransition接收的函數(shù)是同步執(zhí)行的,只是這個(gè)update被加了一個(gè)“transitions"的標(biāo)記。而這個(gè)標(biāo)記,React內(nèi)部處理更新的時(shí)候是會(huì)作為參考信息的。這就意味著,相比于setTimeout, 把一個(gè)update交給startTransition能夠更早地被處理。而在于較快的設(shè)備上,這個(gè)過度是用戶感知不到的。useTransition
在使用startTransition更新狀態(tài)的時(shí)候,用戶可能想要知道transition的實(shí)時(shí)情況,這個(gè)時(shí)候可以使用React提供的hook api?useTransition。
import?{?useTransition?}?from?'react';
const?[isPending,?startTransition]?=?useTransition();
如果transition未完成,isPending值為true,否則為false。
useDeferredValue
使得我們可以延遲更新某個(gè)不那么重要的部分。
相當(dāng)于參數(shù)版的transitions。
舉例:如下圖,當(dāng)用戶在輸入框輸入“書”的時(shí)候,用戶應(yīng)該立馬看到輸入框的反應(yīng),而相比之下,下面的模糊查詢框如果延遲出現(xiàn)一會(huì)兒其實(shí)是完全可以接受的,因?yàn)橛脩艨赡軙?huì)繼續(xù)修改輸入框內(nèi)容,這個(gè)過程中模糊查詢結(jié)果還是會(huì)變化,但是這個(gè)變化對(duì)用戶來說相對(duì)沒那么重要,用戶最關(guān)心的是看到最后的匹配結(jié)果。

用法如下:
import?{useDeferredValue,?useState}?from?"react";
import?MySlowList?from?"../components/MySlowList";
export?default?function?UseDeferredValuePage(props)?{
??const?[text,?setText]?=?useState("hello");
??const?deferredText?=?useDeferredValue(text);
??const?handleChange?=?(e)?=>?{
????setText(e.target.value);
??};
??return?(
????<div>
??????<h3>UseDeferredValuePageh3>
??????{/*?保持將當(dāng)前文本傳遞給?input?*/}
??????<input?value={text}?onChange={handleChange}?/>
??????{/*?但在必要時(shí)可以將列表“延后”?*/}
??????<p>{deferredText}p>
??????<MySlowList?text={deferredText}?/>
????div>
??);
}
MySlowList
import?React,?{memo}?from?"react";
function?ListItem({children})?{
??let?now?=?performance.now();
??while?(performance.now()?-?now?3)?{}
??return?<div?className="ListItem">{children}div>;
}
export?default?memo(function?MySlowList({text})?{
??let?items?=?[];
??for?(let?i?=?0;?i?80;?i++)?{
????items.push(
??????<ListItem?key={i}>
????????Result?#{i}?for?"{text}"
??????ListItem>
????);
??}
??return?(
????<div?className="border">
??????<p>
????????<b>Results?for?"{text}":b>
??????p>
??????<ul?className="List">{items}ul>
????div>
??);
});
Suspense
可以“等待”目標(biāo)UI加載,并且可以直接指定一個(gè)加載的界面(像是個(gè) spinner),讓它在用戶等待的時(shí)候顯示。 }>
??<Comments?/>
</Suspense>
其實(shí)Suspense也早就出現(xiàn)在React中了,只不過之前功能有限。在React18中,背靠Concurrent模式,Suspense終于爆發(fā)了自己的光彩。基本使用:避免等待太久
import?{useState,?Suspense}?from?"react";
import?User?from?"../components/User";
import?Num?from?"../components/Num";
import?{fetchData}?from?"../utils";
import?ErrorBoundaryPage?from?"./ErrorBoundaryPage";
const?initialResource?=?fetchData();
export?default?function?SuspensePage(props)?{
??const?[resource,?setResource]?=?useState(initialResource);
??return?(
????<div>
??????<h3>SuspensePageh3>
??????<ErrorBoundaryPage?fallback={<h1>網(wǎng)絡(luò)出錯(cuò)了h1>}>
????????<Suspense?fallback={<h1>loading?-?userh1>}>
??????????<User?resource={resource}?/>
????????Suspense>
??????ErrorBoundaryPage>
??????<Suspense?fallback={<h1>loading-numh1>}>
????????<Num?resource={resource}?/>
??????Suspense>
??????<button?onClick={()?=>?setResource(fetchData())}>refreshbutton>
????div>
??);
}
錯(cuò)誤處理
每當(dāng)使用 Promises,大概率我們會(huì)用?catch()?來做錯(cuò)誤處理。但當(dāng)我們用 Suspense 時(shí),我們不等待?Promises 就直接開始渲染,這時(shí)?catch()?就不適用了。這種情況下,錯(cuò)誤處理該怎么進(jìn)行呢?在 Suspense 中,獲取數(shù)據(jù)時(shí)拋出的錯(cuò)誤和組件渲染時(shí)的報(bào)錯(cuò)處理方式一樣——你可以在需要的層級(jí)渲染一個(gè)錯(cuò)誤邊界組件來“捕捉”層級(jí)下面的所有的報(bào)錯(cuò)信息。export?default?class?ErrorBoundaryPage?extends?React.Component?{
??state?=?{hasError:?false,?error:?null};
??static?getDerivedStateFromError(error)?{
????return?{
??????hasError:?true,
??????error,
????};
??}
??render()?{
????if?(this.state.hasError)?{
??????return?this.props.fallback;
????}
????return?this.props.children;
??}
}
結(jié)合transitions
所謂提高用戶體驗(yàn),一個(gè)重要的準(zhǔn)則就是保證UI的連續(xù)性,如下面的例子,如果此時(shí)我想把tab從‘photos’切換到‘comments’,但是Comments又沒法立馬渲染出來,這個(gè)時(shí)候不可避免地,就會(huì)Photos頁面消失,顯現(xiàn)Spinner的loading頁面,等一會(huì)兒,Comments頁面才姍姍來遲。function?handleClick()?{
??setTab('comments');
}
}>
??{tab?===?'photos'???<Photos?/>?:?<Comments?/>}
</Suspense>
從UI連續(xù)性上來說,這個(gè)中間出現(xiàn)的Spinner就已經(jīng)破壞了連續(xù)性。而實(shí)際上,正常人的反應(yīng)其實(shí)是沒有那么快,短暫的延遲我們是感覺不到的。所以考慮到UI的連續(xù)性,上面的例子,交互可不可以修改一下,把上面頁面的切換當(dāng)做transitions,這樣即使tab切換,但是依然短暫停留在Photos,之后再改變到Comments:
function?handleClick()?{
??startTransition(()?=>?{
????setTab('comments');
??});
}
const?[isPending,?startTransition]?=?useTransition();
function?handleClick()?{
??startTransition(()?=>?{
????setTab('comments');
??});
}
}>
??<div?style={{?opacity:?isPending???0.8?:?1?}}>
????{tab?===?'photos'???<Photos?/>?:?<Comments?/>}
??div>
</Suspense>SuspenseList
用于控制Suspense組件的顯示順序。
revealOrder?Suspense加載順序
together?所有Suspense一起顯示,也就是最后一個(gè)加載完了才一起顯示全部
forwards?按照順序顯示Suspense
backwards?反序顯示Suspense
tail是否顯示fallback,只在revealOrder為forwards或者backwards時(shí)候有效
hidden不顯示
collapsed輪到自己再顯示
import?{useState,?Suspense,?SuspenseList}?from?"react";
import?User?from?"../components/User";
import?Num?from?"../components/Num";
import?{fetchData}?from?"../utils";
import?ErrorBoundaryPage?from?"./ErrorBoundaryPage";
const?initialResource?=?fetchData();
export?default?function?SuspenseListPage(props)?{
??const?[resource,?setResource]?=?useState(initialResource);
??return?(
????<div>
??????<h3>SuspenseListPageh3>
??????<SuspenseList?tail="collapsed">
????????<ErrorBoundaryPage?fallback={<h1>網(wǎng)絡(luò)出錯(cuò)了h1>}>
??????????<Suspense?fallback={<h1>loading?-?userh1>}>
????????????<User?resource={resource}?/>
??????????Suspense>
????????ErrorBoundaryPage>
????????<Suspense?fallback={<h1>loading-numh1>}>
??????????<Num?resource={resource}?/>
????????Suspense>
??????SuspenseList>
??????<button?onClick={()?=>?setResource(fetchData())}>refreshbutton>
????div>
??);
}四、新的Hooks

關(guān)于useTransition與useDeferredValue上面已經(jīng)介紹過了,接下來說下React18其它的新Hooks,其中useSyncExternalStore與useInsertionEffect屬于Library Hooks。也就是普通應(yīng)用開發(fā)者一般用不到,這倆主要用于那些需要深度融合React模型的庫開發(fā),比如Recoil等。
useId
用于產(chǎn)生一個(gè)在服務(wù)端與Web端都穩(wěn)定且唯一的ID,也支持加前綴,這個(gè)特性多用于支持ssr的環(huán)境下:
export?default?function?NewHookApi(props)?{
??const?id?=?useId();
??return?(
????<div>
??????<h3?id={id}>NewHookApih3>
????div>
??);
}
注意:useId產(chǎn)生的ID不支持css選擇器,如querySelectorAll。
useSyncExternalStore
const?state?=?useSyncExternalStore(subscribe,?getSnapshot[,?getServerSnapshot]);
此Hook用于外部數(shù)據(jù)的讀取與訂閱,可應(yīng)用Concurrent。
基本用法如下:
import?{?useStore?}?from?"../store";
import?{?useId,?useSyncExternalStore?}?from?"../whichReact";
export?default?function?NewHookApi(props)?{
??const?store?=?useStore();
??const?state?=?useSyncExternalStore(store.subscribe,?store.getSnapshot);
??return?(
????<div>
??????<h3>NewHookApih3>
??????<button?onClick={()?=>?store.dispatch({?type:?"ADD"?})}>{state}button>
????div>
??);
}
useStore是我另外定義的,
export?function?useStore()?{
??const?storeRef?=?useRef();
??if?(!storeRef.current)?{
????storeRef.current?=?createStore(countReducer);
??}
??return?storeRef.current;
}
function?countReducer(action,?state?=?0)?{
??switch?(action.type)?{
????case?"ADD":
??????return?state?+?1;
????case?"MINUS":
??????return?state?-?1;
????default:
??????return?state;
??}
}
這里的createStore用的redux思路:
export?function?createStore(reducer)?{
??let?currentState;
??let?listeners?=?[];
??function?getSnapshot()?{
????return?currentState;
??}
??function?dispatch(action)?{
????currentState?=?reducer(action,?currentState);
????listeners.map((listener)?=>?listener());
??}
??function?subscribe(listener)?{
????listeners.push(listener);
????return?()?=>?{
??????//???console.log("unmount",?listeners);
????};
??}
??dispatch({?type:?"TIANNA"?});
??return?{
????getSnapshot,
????dispatch,
????subscribe,
??};
}
對(duì)于還在用自定義store來做低代碼項(xiàng)目的我有點(diǎn)開心,可以用于升級(jí)我的項(xiàng)目了,原先定義的forceUpdate、unsubscribe之類的,可以去掉了~
useInsertionEffect
useInsertionEffect(didUpdate);
函數(shù)簽名同useEffect,但是它是在所有DOM變更前同步觸發(fā)。主要用于css-in-js庫,往DOM中動(dòng)態(tài)注入?或者 SVG?。因?yàn)閳?zhí)行時(shí)機(jī),因此不可讀取refs。
function?useCSS(rule)?{
??useInsertionEffect(()?=>?{
????if?(!isInserted.has(rule))?{
??????isInserted.add(rule);
??????document.head.appendChild(getStyleForRule(rule));
????}
??});
??return?rule;
}
function?Component()?{
??let?className?=?useCSS(rule);
??return?<div?className={className}?/>;
}
具體內(nèi)容可以前往:
https://github.com/reactwg/react-18/discussions/110
文章轉(zhuǎn)載于:高老師:https://juejin.cn/post/7080854114141208612
五、最后
?在我們閱讀完官方文檔后,我們一定會(huì)進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。? ? 這里廣東靚仔給下一些小建議:- 在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計(jì)理念、源碼分層設(shè)計(jì)
- 閱讀下框架官方開發(fā)人員寫的相關(guān)文章
- 借助框架的調(diào)用棧來進(jìn)行源碼的閱讀,通過這個(gè)執(zhí)行流程,我們就完整的對(duì)源碼進(jìn)行了一個(gè)初步的了解
- 接下來再對(duì)源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍
關(guān)注我,一起攜手進(jìn)階
歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
