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:'前端跨端开发指南'}} /> <Comp data={null} /> </div> }如上,在第三个 Comp 组件渲染的时候,因为 data 传入的值是 null ,而在渲染阶段读取了 data 下面的属性,这个时候就会报空指针的错误:Cannot read properties of null ,结果就是整个页面都白屏。
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:'前端跨端开发指南'}} /> <CompSafe data={null} /> </div> }如上,我们将 Comp 组件包装一层,通过 CompSafe 包裹,然后 CompSafe 内容通过 componentDidCatch 来捕获异常,这样就可以将渲染异常产生的影响,由页面维护,降低到了组件维度。其他部分的视图也能够正常渲染了。
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:'前端跨端开发指南'}} /> <CompSafe data={null} /> </div> }
首先,我们用 context 保存一个记录模版状态的方法集合。在页面初始化之后, 接下来会请求数据,在请求数据之后,页面会循环渲染子组件列表,在渲染之前,记录每一个 API 返回的模版,每一个模版需要有一个唯一标识。
每一个渲染模版里面有一个插桩组件,插桩组件在每一个模版下部,确保组件正常渲染,插桩组件一定会渲染。插桩组件的生命周期 componentDidMount 或者 useLayoutEffect 里面,触发事件给最上层组件,并上报该模版的唯一标识。
import React from 'react' /* 上下文保存渲染异常状态 */ export const RenderErrorContext = React.createContext() /* 渲染插桩组件 */ export default function RenderErrorComponent({renderKey}){ const { setRenderKey } = React.useContext(RenderErrorContext) React.useLayoutEffect(()=>{ /* 渲染正常,上报渲染 key */ setRenderKey && setRenderKey(renderKey) },[]) return <React.Fragment /> }如上编写的渲染插桩组件 RenderErrorComponent 和渲染状态上下文 RenderErrorContext ,如果渲染插桩组件正常渲染,那么说明当前组件没有出现渲染异常,接下来需要在 useLayoutEffect 钩子函数里面,上传渲染成功状态。
import React, { useEffect } from 'react' import { RenderErrorContext } from './renderError' import Comp from './component/comp1' /* 模拟的渲染数据 */ const renderList = [ { id:1, data: { value:'我不是外星人' }, }, { id:2, data: { value:'大前端跨端开发指南' }, }, { /* 异常数据 */ id:3, data: null } ] function App() { const [list,setList] = React.useState([]) const renderState = React.useRef({ errorList:[], setRenderKey(id){ //如果渲染成功了,那么将当前 key 移除 const index = renderState.current.errorList.indexOf(id) renderState.current.errorList.splice(index,1) }, getRenderKey(key){ //这里表示渲染了哪些组件 renderState.current.errorList.push(key) } }) useEffect(()=>{ /* 记录每一个待渲染的模版 */ renderList.forEach(item => renderState.current.getRenderKey(item.id)) setList(renderList) /* 验证模版是否正常渲染,如果 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;如上就是页面组件的使用,这里重点介绍一下每一个环节:
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)如上当渲染 Comp 组件的时候,如果 data 为 null, 那么肯定会报出渲染异常,这个时候页面都不会正常显示,为了能够让页面正常展示,我们用一个错误处理组件 ErrorHandle 来防止白屏情况发生。