不少人都用過 React Suspense,但若是你認爲它只是配合 React.lazy 實現異步加載的蒙層,就理解的太淺了。實際上,React Suspense 改變了開發規則,要理解這一點,須要做出思想上的改變。javascript
咱們結合 Why React Suspense Will Be a Game Changer 這篇文章,帶你從新認識 React Suspense。前端
異步加載是前端開發的重要環節,也是一直以來樣板代碼最嚴重的場景之一,原文經過三種取數方案的對比,逐漸找到一種最佳的異步取數方式。java
在講解這三種取數方案以前,首先經過下面這張圖說明了 Suspense 的功能:react
從上圖能夠看出,子元素在異步取數時會阻塞父組件渲染,並一直冒泡到最外層第一個 Suspense,此時 Suspense 不會渲染子組件,而是渲染 fallback
,當全部子組件異步阻塞取消後纔會正常渲染。git
下面介紹文中給出的三種取數方式,首先是最原始的本地狀態管理方案。github
在 Suspense 方案出來以前,咱們通常都在代碼中利用本地狀態管理異步數據。promise
即使代碼作了必定抽象,那也只是把邏輯從一個文件移到了另外一個問題,可維護性與可拓展性都沒有本質的改變,所以基本能夠用下面的結構說明:微信
class DynamicData extends Component {
state = {
loading: true,
error: null,
data: null
};
componentDidMount() {
fetchData(this.props.id)
.then(data => {
this.setState({
loading: false,
data
});
})
.catch(error => {
this.setState({
loading: false,
error: error.message
});
});
}
componentDidUpdate(prevProps) {
if (this.props.id !== prevProps.id) {
this.setState({ loading: true }, () => {
fetchData(this.props.id)
.then(data => {
this.setState({
loading: false,
data
});
})
.catch(error => {
this.setState({
loading: false,
error: error.message
});
});
});
}
}
render() {
const { loading, error, data } = this.state;
return loading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : (
<p>Data loaded ?</p>
);
}
}
複製代碼
如上所述,首先申明本地狀態管理至少三種數據:異步狀態、異步結果與異步錯誤,其次在不一樣的生命週期中處理初始化發請求與從新發請求的問題,最後在渲染函數中根據不一樣的狀態渲染不一樣的結果,因此實際上咱們寫了三個渲染組件。異步
從下面幾個角度對上述代碼進行評價:ide
若是利用 Context 作狀態共享,咱們將取數的數據管理與邏輯代碼寫在父組件,子組件專心用於展現,效果會好一些,代碼以下:
const DataContext = React.createContext();
class DataContextProvider extends Component {
// We want to be able to store multiple sources in the provider,
// so we store an object with unique keys for each data set +
// loading state
state = {
data: {},
fetch: this.fetch.bind(this)
};
fetch(key) {
if (this.state[key] && (this.state[key].data || this.state[key].loading)) {
// Data is either already loaded or loading, so no need to fetch!
return;
}
this.setState(
{
[key]: {
loading: true,
error: null,
data: null
}
},
() => {
fetchData(key)
.then(data => {
this.setState({
[key]: {
loading: false,
data
}
});
})
.catch(e => {
this.setState({
[key]: {
loading: false,
error: e.message
}
});
});
}
);
}
render() {
return <DataContext.Provider value={this.state} {...this.props} />; } } class DynamicData extends Component { static contextType = DataContext; componentDidMount() { this.context.fetch(this.props.id); } componentDidUpdate(prevProps) { if (this.props.id !== prevProps.id) { this.context.fetch(this.props.id); } } render() { const { id } = this.props; const { data } = this.context; const idData = data[id]; return idData.loading ? ( <p>Loading...</p> ) : idData.error ? ( <p>Error: {idData.error}</p> ) : ( <p>Data loaded ?</p> ); } } 複製代碼
DataContextProvider
組件承擔了狀態管理與異步邏輯工做,而 DynamicData
組件只須要從 Context 獲取異步狀態渲染便可,這樣來看至少解決了一部分問題,咱們仍是從以前的角度進行評價:
利用 Suspense 進行異步處理,代碼處理大概是這樣的:
import createResource from "./magical-cache-provider";
const dataResource = createResource(id => fetchData(id));
class DynamicData extends Component {
render() {
const data = dataResource.read(this.props.id);
return <p>Data loaded ?</p>;
}
}
class App extends Component {
render() {
return (
<Suspense fallback={<p>Loading...</p>}> <DeepNesting> <DynamicData /> </DeepNesting> </Suspense>
);
}
}
複製代碼
在原文寫做的時候,Suspense 僅能對 React.lazy 生效,但如今已經能夠對任何異步狀態生效了,只要符合 Pending 中 throw promise 的規則。
咱們再審視一下上面的代碼,能夠發現代碼量減小了不少,其中和轉換成 Function Component 的寫法也有關係。
最後仍是從以下幾個角度進行評價:
爲了進一步說明 Suspense 的魔力,筆者特地把這段代碼單獨拿出來講明:
class App extends Component {
render() {
return (
<Suspense fallback={<p>Loading...</p>}> <DeepNesting> <MaybeSomeAsycComponent /> <Suspense fallback={<p>Loading content...</p>}> <ThereMightBeSeveralAsyncComponentsHere /> </Suspense> <Suspense fallback={<p>Loading footer...</p>}> <DeeplyNestedFooterTree /> </Suspense> </DeepNesting> </Suspense>
);
}
}
複製代碼
上面代碼代表了邏輯與展現的完美分離。
從代碼結構上來看,咱們能夠在任何須要異步取數的組件父級添加 Suspense 達到 Loading 的效果,也就是說,若是隻在最外層加一個 Suspense,那麼整個應用全部 Loading 都結束後纔會渲染,然而咱們也能爲所欲爲的在任何層級繼續添加 Suspense,那麼對應做用域內的 Loading 就會首先執行完畢,並由當前的 Suspense 控制。
這意味着咱們能夠自由決定 Loading 狀態的範圍組合。 試想當 Loading 狀態交由組件控制的方案一與方案二,是不可能作到合併 Loading 時機的,而 Suspense 方案作到了將 Loading 狀態與 UI 分離,咱們能夠經過添加 Suspense 自由控制 Loading 的粒度。
Suspense 對全部子組件異步均可以做用,所以不管是 React.lazy 仍是異步取數,均可以經過 Suspense 進行 Pending。
異步時機被 Suspense pending 須要遵循必定規則,這個規則在以前的 精讀《Hooks 取數 - swr 源碼》 有介紹過,即 Suspense 要求代碼 suspended,即拋出一個能夠被捕獲的 Promise 異常,在這個 Promise 結束後再渲染組件,所以取數函數須要在 Pending 狀態時拋出一個 Promise,使其能夠被 Suspense 捕獲到。
另外,關於文中提到的 fallback 最小出現時間的保護間隔,目前仍是一個 Open Issue,也許有一天 React 官方會提供支持。
不過即使官方不支持,咱們也有方式實現,即讓這個邏輯由 fallback 組件實現:
<Suspense fallback={MyFallback} />;
const MyFallback = () => {
// 計時器,200 ms 之內 return null,200 ms 後 return <Spin />
};
複製代碼
之因此說 Suspense 開發方式改變了開發規則,是由於它作到了將異步的狀態管理與 UI 組件分離,全部 UI 組件都無需關心 Pending 狀態,而是看成同步去執行,這自己就是一個巨大的改變。
另外因爲狀態的分離,咱們能夠利用純 UI 組件拼裝任意粒度的 Pending 行爲,以整個 App 做爲一個大的 Suspense 做爲兜底,這樣 UI 完全與異步解耦,哪裏 Loading,什麼範圍內 Loading,徹底由 Suspense 組合方式決定,這樣的代碼顯然具有了更強的可拓展性。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)