震驚!用 Suspense 解決請(qǐng)求依賴的復(fù)雜場(chǎng)景居然這么簡(jiǎn)單!
有一種復(fù)雜場(chǎng)景 React 新手經(jīng)常處理不好。
那就是一個(gè)頁(yè)面有多個(gè)模塊,每個(gè)模塊都有自己的數(shù)據(jù)需要請(qǐng)求。與此同時(shí),可能部分模塊的數(shù)據(jù)還要依賴父級(jí)的異步數(shù)據(jù)才能正常請(qǐng)求自己的數(shù)據(jù)。如下圖所示,當(dāng)我們直接訪問(wèn)該頁(yè)面時(shí),頁(yè)面請(qǐng)求的數(shù)據(jù)就非常多。而且這些數(shù)據(jù)還有一定的先后依賴關(guān)系。

大概數(shù)據(jù)請(qǐng)求的順序依次如下:
1. 自動(dòng)登錄 -> 個(gè)人用戶信息,權(quán)限信息
2. 左側(cè)路由信息
3. 頁(yè)面頂層數(shù)據(jù)
4. 頁(yè)面五個(gè)模塊各自的數(shù)據(jù)這些接口數(shù)據(jù)依賴關(guān)系比較明確,前面的接口請(qǐng)求完成之后,后續(xù)的接口才能正確請(qǐng)求。
如果頁(yè)面四個(gè)模塊的接口數(shù)據(jù)相互之間沒(méi)有關(guān)系,其實(shí)整個(gè)頁(yè)面還會(huì)簡(jiǎn)單一些,但是很多時(shí)候復(fù)雜度往往來(lái)自于后端的不配合。前端與后端的溝通在一些團(tuán)隊(duì)經(jīng)常出現(xiàn)問(wèn)題。
有的后端不愿意配合前端頁(yè)面結(jié)構(gòu)修改接口,前端也溝通不下來(lái),只能自己咬牙在混亂的接口情況下寫(xiě)頁(yè)面,就導(dǎo)致了無(wú)論是組件的劃分也好還是頁(yè)面的復(fù)雜度也好都變得雜亂無(wú)章。從而增加了開(kāi)發(fā)成本。
因此,只有在一些比較規(guī)范的團(tuán)隊(duì)里,頁(yè)面五個(gè)模塊的數(shù)據(jù)解耦做得比較好。模塊之間干凈簡(jiǎn)潔的依賴關(guān)系能有效降低開(kāi)發(fā)難度。
因此許多前端比較依賴把所有接口都放在父級(jí)組件中去請(qǐng)求的方案,這樣不管你的接口是否混亂,在前端總能處理。但是這樣的結(jié)果就是頁(yè)面組件的耦合變得更加嚴(yán)重。
在 React 19 中,我們可以使用 Suspense 嵌套來(lái)解決這種請(qǐng)求之間前后依賴的方案。我們?cè)陧?xiàng)目中模擬了這種場(chǎng)景的實(shí)現(xiàn)。具體的演示圖如下。

一、重新考慮初始化
和之前的方案一樣,我們先定義父組件的請(qǐng)求接口。
const getMessage = async () => {
const res = await fetch('https://api.chucknorris.io/jokes/random')
return res.json()
}然后在父組件中,將 getMessage() 執(zhí)行之后返回的 promise 作為狀態(tài)存儲(chǔ)在 useState 中。這樣,當(dāng)我點(diǎn)擊時(shí),只需要重新執(zhí)行依次 getMessage() 就可以更新整個(gè)組件。
const [
messagePromise,
setMessagePromise
] = useState(null)但是此時(shí)我們發(fā)現(xiàn),messagePromise 并沒(méi)有初始值,因此初始化時(shí),接口并不會(huì)請(qǐng)求。這種情況下,有兩種交互我們需要探討。一種是通過(guò)點(diǎn)擊按鈕來(lái)初始化接口。另外一種就是組件首次渲染就要初始化接口。
我們之前的案例中,使用了取巧的方式,在函數(shù)組件之外提前獲取了數(shù)據(jù),這會(huì)導(dǎo)致訪問(wèn)任何頁(yè)面該數(shù)據(jù)都會(huì)加載,因此并非合適的手段。
// 我們之前的案例這樣做是一種取巧的方式
const api = getMessage()
function Message() {
...但是如果我們直接把 getMessage() 放在組件內(nèi)部執(zhí)行,也存在不小的問(wèn)題。因?yàn)楫?dāng)組件因?yàn)槠渌臓顟B(tài)發(fā)生變化需要重新執(zhí)行時(shí),此時(shí) getMessage() 也會(huì)冗余的多次執(zhí)行。
// 此時(shí)會(huì)冗余多次執(zhí)行
const [
messagePromise,
setMessagePromise
] = useState(getMessage())理想的情況是 getMessage() 只在組件首次渲染時(shí)執(zhí)行依次,后續(xù)狀態(tài)的改變就不在執(zhí)行。而不需要多次執(zhí)行。
我們先來(lái)考慮通過(guò)點(diǎn)擊事件初始化接口的交互。此時(shí)我們可以先設(shè)置 messagePrmoise 的初始值為 null。
const [
messagePromise,
setMessagePromise
] = useState(null)不過(guò)這樣做有一個(gè)小問(wèn)題就是如果我將 messagePromise 值為 null 時(shí)傳遞給了子組件。那么子組件就會(huì)報(bào)錯(cuò),因此我們需要特殊處理。一種方式就是在子組件內(nèi)部判斷。
const MessageOutput = ({messagePromise}) => {
if (!messagePromise) return
const messageContent = use(messagePromise)或者:
// 這種寫(xiě)法是在需要默認(rèn)顯示狀態(tài)時(shí)的方案
const MessageOutput = ({messagePromise}) => {
const messageContent = messagePromise ? use(messagePromise) : {value: '默認(rèn)值'}另外一種思路就是設(shè)置一個(gè)狀態(tài),子組件基于該狀態(tài)的值來(lái)是否顯示。然后在點(diǎn)擊時(shí)將其設(shè)置為 true。
const [show, setShow] = useState(false)
function __clickHandler() {
setMessagePromise(getMessage())
setShow(true)
}{show && <MessageContainer messagePromise={messagePromise} />}另外一種交互思路就是初始化時(shí)就需要馬上請(qǐng)求數(shù)據(jù)。此時(shí)我們?yōu)榱舜_保 getMessage() 只執(zhí)行一次,可以新增一個(gè)非 state 狀態(tài)來(lái)記錄組件的初始化情況。默認(rèn)值為 false,初始化之后設(shè)置為 true。
const i = useRef(false)
let __api = i.current ? null : getMessage()
const [
messagePromise,
setMessagePromise
] = useState(null)然后在 useEffect 中,將其設(shè)置為 true,表示組件已經(jīng)初始化過(guò)了。
useEffect(() => {
i.current = true
}, [])這是利用 useState 的內(nèi)部機(jī)制,初始化值只會(huì)賦值一次來(lái)做到的。從而我們可以放心更改后續(xù) __api 的值為 null.
從這個(gè)細(xì)節(jié)的角度來(lái)說(shuō),函數(shù)組件多次執(zhí)行的確會(huì)給開(kāi)發(fā)帶來(lái)一些困擾,Vue3/Solid 只執(zhí)行一次的機(jī)制會(huì)更舒適一些,不過(guò)處理得當(dāng)也能避免這個(gè)問(wèn)題。
二、Suspense 嵌套
接下來(lái),我們需要考慮的就是 Suspense 嵌套執(zhí)行的問(wèn)題就行了。這個(gè)執(zhí)行起來(lái)非常簡(jiǎn)單。我們只需要將有異步請(qǐng)求的模塊用 Suspense 包裹起來(lái)當(dāng)成一個(gè)子組件。然后該子組件可以當(dāng)成一個(gè)常規(guī)的子組件作為 Suspense 組件的子組件。
例如,我們聲明一個(gè)子組件如下所示:
const getApi = async () => {
const res = await fetch('https://api.chucknorris.io/jokes/random')
return res.json()
}
export default function Index(props) {
const api = getApi()
return (
<div>
<div id='tips'>多個(gè) Suspense 嵌套,子組件第一部分</div>
<div className="content">
<div className='_05_dou1_message'>父級(jí)消息: {props.value}</div>
<Suspense fallback={<div>Loading...</div>}>
<Item api={api} />
</Suspense>
</div>
</div>
)
}
const Item = ({api}) => {
const joke = api ? use(api) : {value: 'nothing'}
return (
<div className='_03_a_value_update'>子級(jí)消息:{joke.value}</div>
)
}然后我可以將這個(gè)子組件放在 Suspense 內(nèi)就可以了。
import DouPlus1 from './Dou1'
import DouPlus2 from './Dou2'const MessageOutput = ({messagePromise}) => {
const messageContent = use(messagePromise)
return (
<div>
<p>{messageContent.value}</p>
<DouPlus1 value={messageContent.value} />
<DouPlus2 value={messageContent.value} />
</div>
)
}在另外一個(gè)子組件中,我們還設(shè)計(jì)了內(nèi)部狀態(tài),用于實(shí)現(xiàn)切換按鈕,來(lái)增加頁(yè)面交互的復(fù)雜度。并且每次切換都會(huì)請(qǐng)求接口。

如果切換時(shí),上一個(gè)接口沒(méi)有請(qǐng)求完成,React 會(huì)自己處理好數(shù)據(jù)的先后問(wèn)題。不需要我們額外考慮競(jìng)態(tài)條件的情況。完整代碼如下:
var tabs = ['首頁(yè)', '視頻', '探索']
export default function Index() {
var r = useRef(false)
var api = r.current ? null : getApi()
const [promise, setPromise] = useState(api)
const [current, setCurrent] = useState(0)
useEffect(() => {
r.current = true
}, [])
return (
<div>
<div id='tips'>多個(gè) Suspense 嵌套,子組件第二部分</div>
<div className="content">
{tabs.map((item, index) => (
<button
id='btn_05_item'
className={current == index ? 'active' : ''}
onClick={() => {
setCurrent(index)
setPromise(getApi())
}}
key={item}
>{item}</button>
))}
<Suspense fallback={<div className='_05_a_value_item'>Loading...</div>}>
<Item api={promise} />
</Suspense>
</div>
</div>
)
}
const Item = ({api}) => {
const joke = use(api)
return (
<div className='_05_a_value_item'>{joke.value}</div>
)
}三、總結(jié)
當(dāng)我們要在復(fù)雜交互的情況下使用嵌套 Suspense 來(lái)解決問(wèn)題,如果我們組件劃分得當(dāng)、與數(shù)據(jù)依賴關(guān)系處理得當(dāng),那么代碼就會(huì)相當(dāng)簡(jiǎn)單。不過(guò)這對(duì)于開(kāi)發(fā)者來(lái)說(shuō),會(huì)有另外一個(gè)層面的要求。那就是如何合理的處理好組件歸屬問(wèn)題。
許多前端頁(yè)面開(kāi)發(fā)難度往往都是由于組件劃分不合理,屬性歸屬問(wèn)題處理不夠到位導(dǎo)致的。因此 Suspense 在這個(gè)層面有了一個(gè)剛需,開(kāi)發(fā)者必須要具備合理劃分組件的能力,否則即使使用了 Suspense,也依然可能導(dǎo)致頁(yè)面一團(tuán)混亂。

































