React捕獲并處理異常的方式
一 前言
今天來聊一聊在 React 應(yīng)用中,如何發(fā)現(xiàn)異常并處理異常的。
JSX 是優(yōu)勢(shì)也是劣勢(shì)?
在 React 中,出現(xiàn)一次渲染異常的后果是很嚴(yán)重的。比如如下的場(chǎng)景:
function Comp({ data }){
return <div>{ data.value }</div>
}
/* 頁面 */
export default function App(){
return <div>
<div>hello</div>
<Comp data={{value:'hello world'}} />
<Comp data={{value:'前端跨端開發(fā)指南'}} />
<Comp data={null} />
</div>
}
如上,在第三個(gè) Comp 組件渲染的時(shí)候,因?yàn)?data 傳入的值是 null ,而在渲染階段讀取了 data 下面的屬性,這個(gè)時(shí)候就會(huì)報(bào)空指針的錯(cuò)誤:Cannot read properties of null ,結(jié)果就是整個(gè)頁面都白屏。


這樣后果是嚴(yán)重的,所以 React 中要特別注意渲染數(shù)據(jù)的規(guī)范與嚴(yán)謹(jǐn)。
這個(gè)問題本質(zhì)上和 React 采用 JSX 語法而并非渲染模版有一定的關(guān)系。JSX 給 React 帶來很便利的開發(fā)體驗(yàn),開發(fā)者可以借助 JSX 靈活使用組合模式,render props 模式,Hoc 等各種設(shè)計(jì)模式,JSX 給開發(fā)者帶來了很大的發(fā)揮空間,但是凡事都有兩面性。JSX 的靈活性也帶來一定的潛在風(fēng)險(xiǎn)。
React jsx 在編譯階段,會(huì)被 babel 變成 React.Element 的形式,它的執(zhí)行是在 React 整個(gè)渲染的 render 階段執(zhí)行的,如果 React.Element 出現(xiàn)了空指針等異常,那么就會(huì)中斷 render 階段的執(zhí)行,當(dāng)然也不會(huì)執(zhí)行渲染真實(shí) DOM 的 commit 階段。所以如果是初次渲染,任何渲染動(dòng)作也就不會(huì)執(zhí)行,最終呈現(xiàn)給我們的視圖就是白屏。
那么如何處理這個(gè)問題呢?
二 渲染異常處理
componentDidCatch
還好 React 中提供了 componentDidCatch 或者 getDerivedStateFromError 生命周期,去挽救由于渲染階段出現(xiàn)問題造成 UI 界面無法顯示的情況。 我們以 componentDidCatch 為例子,看一下它是如何處理的異常。
componentDidCatch 是 React 類組件的生命周期,它接受兩個(gè)參數(shù):
1 error —— 拋出的錯(cuò)誤。 2 info —— 帶有 componentStack key 的對(duì)象,其中包含有關(guān)組件引發(fā)錯(cuò)誤的棧信息。 先來打印一下,生命周期 componentDidCatch 參數(shù)長(zhǎng)什么樣子。
那么 componentDidCatch 中可以再次觸發(fā) setState,來降級(jí) UI 渲染,componentDidCatch 會(huì)在 commit 階段被調(diào)用,因此允許執(zhí)行副作用。我們給上面的例子用類組件和 componentDidCatch 改造,如下:
function Comp({ data }){
return <div>{ data.value }</div>
}
class CompSafe extends React.Component{
state = {
isError:false
}
componentDidCatch(){
this.setState({ isError:true })
}
render(){
const { isError } = this.state
return isError ? null : <Comp {...this.props} />
}
}
export default function App(){
return <div>
<div>hello</div>
<CompSafe data={{value:'hello world'}} />
<CompSafe data={{value:'前端跨端開發(fā)指南'}} />
<CompSafe data={null} />
</div>
}
如上,我們將 Comp 組件包裝一層,通過 CompSafe 包裹,然后 CompSafe 內(nèi)容通過 componentDidCatch 來捕獲異常,這樣就可以將渲染異常產(chǎn)生的影響,由頁面維護(hù),降低到了組件維度。其他部分的視圖也能夠正常渲染了。

但是這樣同樣暴露出一個(gè)問題。就是我們把所有的組件,都像 Comp 一樣,在配套一個(gè)渲染異常的組件 CompSafe, 那樣是不切實(shí)際的,所以我們需要一個(gè)通用能力,這樣就需要一個(gè)渲染異常的高級(jí)組件來解決。
hoc 高階組件模式也是 React 比較常用的一種包裝強(qiáng)化模式之一,高階函數(shù)是接收一個(gè)函數(shù),返回一個(gè)函數(shù),而所謂高階組件,就是接收一個(gè)組件,返回一個(gè)組件,返回的組件是根據(jù)需要對(duì)原始組件的強(qiáng)化。
HOC 助力渲染異常組件
我們接下來編寫一個(gè)通用高階組件,解決渲染異常。
function SafeCompHoc(Comp) {
return class CompSafe extends React.Component{
state = {
isError:false
}
componentDidCatch(){
this.setState({ isError:true })
}
render(){
const { isError } = this.state
return isError ? <div>渲染異常</div> : <Comp {...this.props} />
}
}
}
const CompSafe = SafeCompHoc(Comp)
export default function App(){
return <div>
<div>hello</div>
<CompSafe data={{value:'hello world'}} />
<CompSafe data={{value:'前端跨端開發(fā)指南'}} />
<CompSafe data={null} />
</div>
}

如上,經(jīng)過 SafeCompHoc 包裝之后的,可以批量處理渲染異常的組件,可能出現(xiàn)渲染異常的核心組件,就可以用 SafeCompHoc 統(tǒng)一處理了。
三 渲染異常監(jiān)控
渲染監(jiān)控:
如上通過 HOC 的方式做到了渲染降級(jí),但是如果只做到監(jiān)控級(jí)別,那是遠(yuǎn)遠(yuǎn)不夠的,我們要做的就是,發(fā)現(xiàn)問題,去根本解決問題,這種渲染問題大概率可能是渲染數(shù)據(jù)結(jié)構(gòu)出現(xiàn)了問題,而數(shù)據(jù)結(jié)構(gòu)大概率又是后端返回的,所以這個(gè)異常本質(zhì)上很可能是服務(wù)端出了問題。
這個(gè)時(shí)候,發(fā)現(xiàn)問題也是非常重要的,那么就需要一個(gè)渲染的監(jiān)控方法。 接下來我們將用context 上下文 + 插槽的方案來實(shí)現(xiàn)一個(gè)渲染模版監(jiān)控方案。
渲染插槽+context上下文
技術(shù)方案: 核心技術(shù)實(shí)現(xiàn):context + 插樁組件
- 首先,我們用 context 保存一個(gè)記錄模版狀態(tài)的方法集合。在頁面初始化之后, 接下來會(huì)請(qǐng)求數(shù)據(jù),在請(qǐng)求數(shù)據(jù)之后,頁面會(huì)循環(huán)渲染子組件列表,在渲染之前,記錄每一個(gè) API 返回的模版,每一個(gè)模版需要有一個(gè)唯一標(biāo)識(shí)。
- 每一個(gè)渲染模版里面有一個(gè)插樁組件,插樁組件在每一個(gè)模版下部,確保組件正常渲染,插樁組件一定會(huì)渲染。插樁組件的生命周期 componentDidMount 或者 useLayoutEffect 里面,觸發(fā)事件給最上層組件,并上報(bào)該模版的唯一標(biāo)識(shí)。
- 根組件在完成首次渲染之后,通過短暫的延時(shí)后,對(duì)比渲染列表里面的每一個(gè)模版的標(biāo)識(shí),是否均備插樁組件上報(bào),如果有個(gè)別組件的標(biāo)識(shí)沒有上報(bào),則認(rèn)為是該組件渲染異常。如果有子組件發(fā)生渲染異常,上報(bào)該子組件的渲染數(shù)據(jù)。方便查詢問題。
原理圖:

介紹完原理來看一下代碼的實(shí)現(xiàn):
渲染插樁組件:
import React from 'react'
/* 上下文保存渲染異常狀態(tài) */
export const RenderErrorContext = React.createContext()
/* 渲染插樁組件 */
export default function RenderErrorComponent({renderKey}){
const { setRenderKey } = React.useContext(RenderErrorContext)
React.useLayoutEffect(()=>{
/* 渲染正常,上報(bào)渲染 key */
setRenderKey && setRenderKey(renderKey)
},[])
return <React.Fragment />
}
如上編寫的渲染插樁組件 RenderErrorComponent 和渲染狀態(tài)上下文 RenderErrorContext ,如果渲染插樁組件正常渲染,那么說明當(dāng)前組件沒有出現(xiàn)渲染異常,接下來需要在 useLayoutEffect 鉤子函數(shù)里面,上傳渲染成功狀態(tài)。
接下來看一下使用渲染上下文的頁面組件。
import React, { useEffect } from 'react'
import { RenderErrorContext } from './renderError'
import Comp from './component/comp1'
/* 模擬的渲染數(shù)據(jù) */
const renderList = [
{
id:1,
data: {
value:'我不是外星人'
},
},
{
id:2,
data: {
value:'大前端跨端開發(fā)指南'
},
},
{ /* 異常數(shù)據(jù) */
id:3,
data: null
}
]
function App() {
const [list,setList] = React.useState([])
const renderState = React.useRef({
errorList:[],
setRenderKey(id){ //如果渲染成功了,那么將當(dāng)前 key 移除
const index = renderState.current.errorList.indexOf(id)
renderState.current.errorList.splice(index,1)
},
getRenderKey(key){ //這里表示渲染了哪些組件
renderState.current.errorList.push(key)
}
})
useEffect(()=>{
/* 記錄每一個(gè)待渲染的模版 */
renderList.forEach(item => renderState.current.getRenderKey(item.id))
setList(renderList)
/* 驗(yàn)證模版是否正常渲染,如果 errorList 不為空,那么有渲染異常的組件,里面的 item 就是渲染異常的 id */
setTimeout(()=>{
console.log('errorList',renderState.current.errorList)
})
},[])
return (
<RenderErrorContext.Provider value={renderState.current}>
{ list.map(item=><Comp data={item.data} id={item.id} key={item.id} />) }
</RenderErrorContext.Provider>
);
}
export default App;
如上就是頁面組件的使用,這里重點(diǎn)介紹一下每一個(gè)環(huán)節(jié):
- 首先,用 ref 保存渲染狀態(tài) renderState,是一個(gè)對(duì)象,在對(duì)象里面一定要有 setRenderKey 方法,提供給插槽組件使用。最終將渲染狀態(tài)傳遞給 RenderErrorContext 的 Provider 中,接下來每一個(gè)需要監(jiān)控的下游組件都可以回傳渲染狀態(tài)了。
- 在 useEffect 模擬請(qǐng)求數(shù)據(jù),然后根據(jù)數(shù)據(jù),記錄下來待渲染的 id,通過 getRenderKey 將 id 放入到數(shù)組中。
- 接下來當(dāng)插樁組件正常渲染,那么會(huì)回傳狀態(tài),證明渲染成功了,那么將此渲染 id 從數(shù)組中移除。
- 接下來用 setTimeout 驗(yàn)證模版是否正常渲染,如果 errorList 不為空,那么有渲染異常的組件,里面的 item 就是渲染異常的 id 。
- 在渲染列表中,我們模擬一條異常數(shù)據(jù),就是第三條,data 為 null。
接下來看一下渲染插樁組件的使用:
import React from 'react'
import RenderErrorComponent from '../renderError'
function Comp({ data, id }){
return <div>
<div>{ data.value } </div>
<RenderErrorComponent renderKey={id} />
</div>
}
function ErrorHandle (Component){
return class Wrap extends React.Component{
state = {
isError:false
}
componentDidCatch(){
this.setState({isError : true })
}
render(){
const { isError } = this.state
return isError ? null : <Component {...this.props} />
}
}
}
export default ErrorHandle(Comp)
如上當(dāng)渲染 Comp 組件的時(shí)候,如果 data 為 null, 那么肯定會(huì)報(bào)出渲染異常,這個(gè)時(shí)候頁面都不會(huì)正常顯示,為了能夠讓頁面正常展示,我們用一個(gè)錯(cuò)誤處理組件 ErrorHandle 來防止白屏情況發(fā)生。
看一下效果:


如上頁面能夠正常渲染,從渲染異常列表里,能夠查詢到渲染異常的組件 id=3,預(yù)期達(dá)成。
四 總結(jié)
以上就是React捕獲并處理異常的方式的詳細(xì)內(nèi)容,更多關(guān)于React捕獲并處理異常的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react-redux及redux狀態(tài)管理工具使用詳解
Redux是為javascript應(yīng)用程序提供一個(gè)狀態(tài)管理工具集中的管理react中多個(gè)組件的狀態(tài)redux是專門作狀態(tài)管理的js庫(不是react插件庫可以用在其他js框架中例如vue,但是基本用在react中),這篇文章主要介紹了react-redux及redux狀態(tài)管理工具使用詳解,需要的朋友可以參考下2023-01-01
React 進(jìn)入頁面后自動(dòng) focus 到某個(gè)輸入框的解決方案
React.js 當(dāng)中提供了 ref 屬性來幫助我們獲取已經(jīng)掛載的元素的 DOM 節(jié)點(diǎn),你可以給某個(gè) JSX 元素加上 ref屬性,這篇文章主要介紹了React 進(jìn)入頁面以后自動(dòng) focus 到某個(gè)輸入框,需要的朋友可以參考下2024-02-02
react中useState使用:如何實(shí)現(xiàn)在當(dāng)前表格直接更改數(shù)據(jù)
這篇文章主要介紹了react中useState的使用:如何實(shí)現(xiàn)在當(dāng)前表格直接更改數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
詳解react內(nèi)聯(lián)樣式使用webpack將px轉(zhuǎn)rem
這篇文章主要介紹了詳解react內(nèi)聯(lián)樣式使用webpack將px轉(zhuǎn)rem,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09
詳解React中的useMemo和useCallback的區(qū)別
React中的useMemo和useCallback是兩個(gè)重要的Hooks。常常被用于優(yōu)化組件的性能。雖然這兩個(gè)Hooks看起來很相似,但它們彼此之間還是有很大的區(qū)別的,隨著小編一起來學(xué)習(xí)吧2023-04-04
react中實(shí)現(xiàn)將一個(gè)視頻流為m3u8格式的轉(zhuǎn)換
這篇文章主要介紹了react中實(shí)現(xiàn)將一個(gè)視頻流為m3u8格式的轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
React創(chuàng)建對(duì)話框組件的方法實(shí)例
在項(xiàng)目開發(fā)過程中,對(duì)于復(fù)雜的業(yè)務(wù)選擇功能很常見,下面這篇文章主要給大家介紹了關(guān)于React創(chuàng)建對(duì)話框組件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05

