React 異步組件前世與今生
一 前言
今天我們聊一聊 React 中的異步組件的現況和未來,異步組件很可能是未來從數據交互到 UI 展示一種流暢的技術方案,所以既然要喫透 React,進階 React,就有必要搞懂異步組件。
老規矩,我們還是帶着問題開始今天的思考?(自測掌握程度)
-
1 什麼是 React 異步組件,解決什麼問題?
-
2 componentDidCatch 如何捕獲到渲染階段錯誤,又這麼彌補。
-
3 React.lazy 如何實現動態加載的?
-
4 React.lazy 爲什麼要在 Supsonse 內部。
-
5 Supsonse 原理是什麼?
二 初識:異步組件
1 什麼是異步組件
我們先來想想目前的 React 應用中使用ajax
或者fetch
進行數據交互場景,基本上就是這樣的,在類組件中componentDidMount
和函數組件effect
中進行數據交互,得到數據後,再渲染 UI 視圖。那麼可不可以讓組件的渲染等待異步數據請求完畢,得到數據後再進行 render 呢?
對於上面這種情況,第一感覺是難以置信,如果能夠實現讓渲染中斷,等到數據請求之後,再渲染呢?那就是Susponse
,上面說到的不可能實現的事,Susponse
做到了,React 16.6 新增了,Susponse
讓組件 “等待” 某個異步操作,直到該異步操作結束即可渲染。
傳統模式:渲染組件 -> 請求數據 -> 再渲染組件。
異步模式:請求數據 -> 渲染組件。
2 開啓 Suspense 模式
一個傳統模式下的數據交互應該是這個樣子的。
function Index(){
const [ userInfo , setUserInfo ] = React.useState(0)
React.useEffect(()=>{
/* 請求數據交互 */
getUserInfo().then(res=>{
setUserInfo(res)
})
},[])
return <div>
<h1>{userInfo.name}</h1>;
</div>
}
export default function Home(){
return <div>
<Index />
</div>
}
- 流程:頁面初始化掛載,
useEffect
裏面請求數據,通過useState
改變數據,二次更新組件渲染數據。
那麼如果用Susponse
異步模式就可以這麼寫:
function FutureAsyncComponent (){
const userInfo = getUserInfo()
return <div>
<h1>{userInfo.name}</h1>;
</div>
}
/* 未來的異步模式 */
export default function Home(){
return <div>
<React.Suspense fallback={ <div > loading... </div> } >
<FutureAsyncComponent/>
</React.Suspense>
</div>
}
當數據還沒有加載完成時候,會展示Suspense
中 fallback
的內容,彌補請求數據中過渡效果 ,儘管這個模式在現在版本中還不能正式使用,但是將來 React 會支持這樣的代碼形式。
三 溯源:從 componentDidCatch 到 Suspense
至於 Suspense 是如何將上述不可能的事情變成可能的呢?這就要從 componentDidCatch
說起了,在 React 推出 v16 的時候,就增加了一個新生命週期函數 componentDidCatch
。如果某個組件定義了 componentDidCatch
,那麼這個組件中所有的子組件在渲染過程中拋出異常時,這個 componentDidCatch
函數就會被調用。
componentDidCatch 使用
componentDidCatch 可以捕獲異常,它接受兩個參數:
-
1 error —— 拋出的錯誤。
-
2 info —— 帶有 componentStack key 的對象,其中包含有關組件引發錯誤的棧信息。
我們來模擬一個子組件渲染失敗的情況:
/* 正常組件,可以渲染 */
function Children(){
return <div> hello ,let us learn React </div>
}
/* 非React組件,將無法正常渲染 */
function Children1(){
return
}
export default class Index extends React.Component{
componentDidCatch(error,info){
console.log(error,info)
}
render(){
return <div>
<Children />
<Children1/>
</div>
}
}
如上,我們模擬一個 render 失敗的場景,將一個非 React 組件 Children1 當作正常的 React 的組件來渲染,這樣在渲染階段就會報錯,錯誤信息就會被 componentDidCatch
捕獲到,錯誤信息如下:
1.jpg
對於如上如果在渲染子組件的時候出現錯誤,會導致整個組件渲染失敗,無法顯示,正常的組件Children
也會被牽連,這個時候我們需要在componentDidCatch
做一些補救措施,比如我們發現 componentDidCatch
失敗,可以給Children1
加一個狀態控制,如果渲染失敗,那麼終止Children1
的 render。
function ErroMessage(){
return <div>渲染出現錯誤~</div>
}
export default class Index extends React.Component{
state={ errorRender:false }
componentDidCatch(error,info){
/* 補救措施 */
this.setState({
errorRender:true
})
}
render(){
return <div>
<Children />
{ this.state.errorRender ? <ErroMessage/> : <Children1/> }
</div>
}
}
2.jpg
如果出現錯誤,通過setState
重新渲染,並移除失敗的組件,這樣組件就能正常渲染了,同樣也不影響 Children 掛載。componentDidCatch
一方面捕獲在渲染階段出現的錯誤,另一方面可以在生命週期的內部執行副作用去挽回渲染異常帶來的損失。
componentDidCatch 原理
componentDidCatch
原理應該很好理解,內部可以通過try{}catch(error){}
來捕獲渲染錯誤,處理渲染錯誤。
try {
//嘗試渲染子組件
} catch (error) {
// 出現錯誤,componentDidCatch被調用,
}
componentDidCatch 思想能否遷移到 Suspense 上
那麼回到我們的異步組件上來,如果讓異步的代碼放在同步執行,是肯定不會正常的渲染的,我們還是要先請求數據,等到數據返回,再用返回的數據進行渲染,那麼重點在於這個等字,如何讓同步的渲染停止下來,去等異步的數據請求呢?拋出異常可以嗎? 異常可以讓代碼停止執行,當然也可以讓渲染中止。
Suspense
就是用拋出異常的方式中止的渲染,Suspense
需要一個 createFetcher
函數會封裝異步操作,當嘗試從 createFetcher
返回的結果讀取數據時,有兩種可能:一種是數據已經就緒,那就直接返回結果;還有一種可能是異步操作還沒有結束,數據沒有就緒,這時候 createFetcher
會拋出一個 “異常”。
這個 “異常” 是正常的代碼錯誤嗎?非也,這個異常是封裝請求數據的 Promise 對象,裏面是真正的數據請求方法,既然 Suspense 能夠拋出異常,就能夠通過類似 componentDidCatch
的try{}catch{}
去獲取這個異常。
獲取這個異常之後幹什麼呢? 我們知道這個異常是Promise
,那麼接下來當然是執行這個Promise
,在成功狀態後,獲取數據,然後再次渲染組件,此時的渲染就已經讀取到正常的數據,那麼可以正常的渲染了。接下來我們模擬一下createFetcher
和Suspense
我們模擬一個簡單 createFetcher
/**
*
* @param {*} fn 我們請求數據交互的函數,返回一個數據請求的Promise
*/
function createFetcher(fn){
const fetcher = {
status:'pedding',
result:null,
p:null
}
return function (){
const getDataPromise = fn()
fetcher.p = getDataPromise
getDataPromise.then(result=>{ /* 成功獲取數據 */
fetcher.result = result
fetcher.status = 'resolve'
})
if(fetcher.status === 'pedding'){ /* 第一次執行中斷渲染,第二次 */
throw fetcher
}
/* 第二次執行 */
if(fetcher.status)
return fetcher.result
}
}
-
返回一個函數,在渲染階段執行,第一次組件渲染,由於
status = pedding
所以拋出異常fetcher
給Susponse
,渲染中止。 -
Susponse
會在內部componentDidCatch
處理這個 fetcher,執行getDataPromise.then
, 這個時候status
已經是resolve
狀態,數據也能正常返回了。 -
接下來
Susponse
再次渲染組件,此時,此時就能正常的獲取數據了。
我們模擬一個簡單的 Suspense
export class Suspense extends React.Component{
state={ isRender: true }
componentDidCatch(e){
/* 異步請求中,渲染 fallback */
this.setState({ isRender:false })
const { p } = e
Promise.resolve(p).then(()=>{
/* 數據請求後,渲染真實組件 */
this.setState({ isRender:true })
})
}
render(){
const { isRender } = this.state
const { children , fallback } = this.props
return isRender ? children : fallback
}
}
- 用 componentDidCatch 捕獲異步請求,如果有異步請求渲染 fallback,等到異步請求執行完畢,渲染真實組件,藉此整個異步流程完畢。但爲了讓大家明白流程,只是一次模擬異步的過程,實際流程要比這個複雜的多。
流程圖:
3.jpg
四 實踐:從 Suspense 到 React.lazy
React.lazy 簡介
Suspense
帶來的異步組件的革命還沒有一個實質性的成果,目前版本沒有正式投入使用,但是 React.lazy 是目前版本 Suspense 的最佳實踐。我們都知道React.lazy
配合Suspense
可以實現懶加載,按需加載,這樣很利於代碼分割,不會讓初始化的時候加載大量的文件,減少首屏時間。
React.lazy 基本使用
const LazyComponent = React.lazy(()=>import('./text'))
React.lazy
接受一個函數,這個函數需要動態調用 import()
。它必須返回一個 Promise
,該 Promise
需要 resolve
一個 default export
的 React
組件。
我們先來看一下基本使用:
const LazyComponent = React.lazy(() => import('./test.js'))
export default function Index(){
return <Suspense fallback={<div>loading...</div>} >
<LazyComponent />
</Suspense>
}
我們用Promise
模擬一下 import()
效果,將如上 LazyComponent
改成如下的樣子:
function Test(){
return <div class >《React進階實踐指南》即將上線~</div>
}
const LazyComponent = React.lazy(()=> new Promise((resolve)=>{
setTimeout(()=>{
resolve({
default: ()=> <Test />
})
},2000)
}))
效果:
5.gif
React.lazy 原理解讀
React.lazy 是如何配合 Susponse 實現動態加載的效果的呢?實際上, lazy 內部就是做了一個 createFetcher,而上面講到 createFetcher 得到渲染的數據,而 lazy 裏面自帶的 createFetcher 異步請求的是組件。lazy 內部模擬一個 promiseA 規範場景。我們完全可以理解 React.lazy 用 Promise 模擬了一個請求數據的過程,但是請求的結果不是數據,而是一個動態的組件。
接下來我們看一下 lazy 是如何處理的
react/src/ReactLazy.js
function lazy(ctor){
return {
$$typeof: REACT_LAZY_TYPE,
_payload:{
_status: -1, //初始化狀態
_result: ctor,
},
_init:function(payload){
if(payload._status===-1){ /* 第一次執行會走這裏 */
const ctor = payload._result;
const thenable = ctor();
payload._status = Pending;
payload._result = thenable;
thenable.then((moduleObject)=>{
const defaultExport = moduleObject.default;
resolved._status = Resolved; // 1 成功狀態
resolved._result = defaultExport;/* defaultExport 爲我們動態加載的組件本身 */
})
}
if(payload._status === Resolved){ // 成功狀態
return payload._result;
}
else { //第一次會拋出Promise異常給Suspense
throw payload._result;
}
}
}
}
React.lazy
包裹的組件會標記REACT_LAZY_TYPE
類型的 element,在調和階段會變成LazyComponent
類型的fiber
,React
對LazyComponent
會有單獨的處理邏輯,第一次渲染首先會執行_init
方法。此時這個_init
方法就可以理解成createFetcher
。
我們看一下 lazy 中 init 函數的執行:
react-reconciler/src/ReactFiberBeginWork.js
function mountLazyComponent(){
const init = lazyComponent._init;
let Component = init(payload);
}
-
如上在
mountLazyComponent
初始化的時候執行_init
方法,裏面會執行lazy
的第一個函數,得到一個Promise
,綁定Promise.then
成功回調,回調裏得到我們組件defaultExport
,這裏要注意的是,如上面的函數當第二個 if 判斷的時候,因爲此時狀態不是Resolved
,所以會走else
,拋出異常 Promise,拋出異常會讓當前渲染終止。 -
Susponse
內部處理這個promise
,然後再一次渲染組件,下一次渲染就直接渲染這個組件。達到了動態加載的目的。
流程圖
4.jpg
五 展望:Suspense 未來可期
你當下並不使用 Relay,那麼你暫時無法在應用中試用 Suspense。因爲迄今爲止,在實現了 Suspense 的庫中,Relay 是我們唯一在生產環境測試過,且對它的運作有把握的一個庫。
目前 Suspense 還並不能,如果你想使用,可以嘗試一下在生產環境使用集成了 Suspense
的 Relay
。Relay 指南!
Suspense 能解決什麼?
-
Suspense
讓數據獲取庫與React
緊密整合。如果一個數據請求庫實現了對Suspense
的支持,那麼,在React
中使用Suspense
將會是自然不過的事。 -
Suspense
能夠自由的展現,請求中的加載效果。能讓視圖加載有更主動的控制權。 -
Suspense
能夠讓請求數據到渲染更流暢靈活,我們不用在componentDidMount
請求數據,再次觸發 render,一切交給Suspense
解決,一氣呵成。
Suspense 面臨挑戰?
對於未來的Suspense
能否作爲主流異步請求數據渲染的方案,筆者認爲 Suspense 未來還是充滿期待,那麼對於 Suspense 的挑戰,個人感覺在於以下幾個方面:
-
1
concurrent
模式下的Susponse
可以帶來更好的用戶體驗,react 團隊能夠讓未來的 Suspense 更靈活,有一套更清晰明確的createFetcher
製作手冊,是未來的concurrent
模式下Suspense
脫穎而出的關鍵。 -
2
Suspense
能否廣泛使用,更在於Suspense
的生態發展,有一個穩定的數據請求庫與Suspense
完美契合。 -
3 開發者對
Suspense
的價值的認可,如果Suspense
在未來的表現力更出色的話,會有更多開發者寧願自己封裝一套數據請求方法,給優秀的Suspense
買單。
六 總結
本文講了 React Susponse 的由來,實現原理,目前階段狀態,以及未來的展望,對於 React 前世與今生,你有什麼看法呢?
往期 React 進階文章
「React 進階」 React 全部 api 解讀 + 基礎實踐大全 (夯實基礎萬字總結)
「React 進階」一文喫透 React 高階組件 (HOC)
「React 進階」年終送給 react 開發者的八條優化建議
參考
React 中文文檔
我是卡頌,《React 技術揭祕》作者,全球開發者資訊觀察者
加我個人微信,我會:
-
每天在朋友圈分享
全球最新開發者資訊
-
拉你進
React源碼級進階羣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RR8uH6LcdGqgle0nY05tqw