Error Boundaries是這么實(shí)現(xiàn)的,還挺巧妙
大家好,我卡頌。
本文會(huì)講解React中Error Boundaries的完整實(shí)現(xiàn)邏輯。
一張圖概括:
這里簡(jiǎn)單講解下React工作流程,后文有用。分為三步:
- 觸發(fā)更新
- render階段:計(jì)算更新會(huì)造成的副作用
- commit階段:在宿主環(huán)境執(zhí)行副作用
副作用有很多,比如:
- 插入DOM節(jié)點(diǎn)
- 執(zhí)行useEffect回調(diào)
好了,讓我們進(jìn)入主題。
什么是Error BoundariesReact
提供了兩個(gè)與「錯(cuò)誤處理」相關(guān)的API:
- getDerivedStateFromError:靜態(tài)方法,當(dāng)錯(cuò)誤發(fā)生后提供一個(gè)機(jī)會(huì)渲染fallback UI
- componentDidCatch:組件實(shí)例方法,當(dāng)錯(cuò)誤發(fā)生后提供一個(gè)機(jī)會(huì)記錄錯(cuò)誤信息
使用了這兩個(gè)API的ClassComponent通常被稱為Error Boundaries(錯(cuò)誤邊界)。
在Error Boundaries的「子孫組件」中發(fā)生的所有「React工作流程內(nèi)」的錯(cuò)誤都會(huì)被Error Boundaries捕獲。
通過(guò)開篇的介紹可以知道,「React工作流程」指:
- render階段
- commit階段
考慮如下代碼:
- class ErrorBoundary extends Component {
- componentDidCatch(e) {
- console.warn(“發(fā)生錯(cuò)誤”, e);
- }
- render() {
- return <div>{this.props.children}</div>;
- }
- }
- const App = () => (
- <ErrorBoundary>
- <A><B/></A>
- <C/>
- <ErrorBoundary>
- )
A、B、C作為ErrorBoundary的子孫組件,當(dāng)發(fā)生「React工作流程內(nèi)」的錯(cuò)誤,都會(huì)被ErrorBoundary中的componentDidCatch方法捕獲。
步驟1:捕獲錯(cuò)誤
首先來(lái)看「工作流程中的錯(cuò)誤都是何時(shí)被捕獲的」。
render階段的核心代碼如下,發(fā)生的錯(cuò)誤會(huì)被handleError處理:
- do {
- try {
- // 對(duì)于并發(fā)更新則是workLoopConcurrent
- workLoopSync();
- break;
- } catch (thrownValue) {
- handleError(root, thrownValue);
- }
- } while (true);
commit階段包含很多工作,比如:
- componentDidMount/Update執(zhí)行
- 綁定/解綁ref
- useEffect/useLayoutEffect callback與destroy執(zhí)行
這些工作會(huì)以如下形式執(zhí)行,發(fā)生的錯(cuò)誤被captureCommitPhaseError處理:
- try {
- // …執(zhí)行某項(xiàng)工作
- } catch (error) {
- captureCommitPhaseError(fiber, fiber.return, error);
- }
步驟2:構(gòu)造callback
可以發(fā)現(xiàn),即使沒(méi)有Error Boundaries,「工作流程」中的錯(cuò)誤已經(jīng)被React捕獲了。而正確的邏輯應(yīng)該是:
- 如果存在Error Boundaries,執(zhí)行對(duì)應(yīng)API
- 拋出React的提示信息
- 如果不存在Error Boundaries,拋出「未捕獲的錯(cuò)誤」
所以,不管是handleError還是captureCommitPhaseError,都會(huì)從發(fā)生錯(cuò)誤的節(jié)點(diǎn)的父節(jié)點(diǎn)開始,逐層向上遍歷,尋找最近的Error Boundaries。
一旦找到,就會(huì)構(gòu)造:
- 用于「執(zhí)行Error Boundaries API」的callback
- 用于「拋出React提示信息」的callback
- // ...為了可讀性,邏輯有刪減
- function createClassErrorUpdate() {
- if (typeof getDerivedStateFromError === 'function') {
- // 用于執(zhí)行g(shù)etDerivedStateFromError的callback
- update.payload = () => {
- return getDerivedStateFromError(error);
- };
- // 用于拋出React提示信息的callback
- update.callback = () => {
- logCapturedError(fiber, errorInfo);
- };
- }
- if (inst !== null && typeof inst.componentDidCatch === 'function') {
- // 用于執(zhí)行componentDidCatch的callback
- update.callback = function callback() {
- this.componentDidCatch(error);
- };
- }
- return update;
- }
如果沒(méi)有找到Error Boundaries,繼續(xù)向上遍歷直到根節(jié)點(diǎn)。
此時(shí)會(huì)構(gòu)造:
- 用于「拋出未捕獲錯(cuò)誤」的callback
- 用于「拋出React提示信息」的callback
- // ...為了可讀性,邏輯有刪減
- funffction createRootErrorUpdate() {
- // 用于拋出“未捕獲的錯(cuò)誤”及“React的提示信息”的callback
- update.callback = () => {
- onUncaughtError(error);
- logCapturedError(fiber, errorInfo);
- };
- return update;
- }
執(zhí)行callback
構(gòu)造好的callback在什么時(shí)候執(zhí)行呢?
在React中有兩個(gè)「執(zhí)行用戶自定義callback」的API:
- 對(duì)于ClassComponent, this.setState(newState, callback)中newState和callback參數(shù)都能傳遞Function作為callback
所以,對(duì)于Error Boundaries,相當(dāng)于主動(dòng)觸發(fā)了一次更新:
- this.setState(() => {
- // 用于執(zhí)行g(shù)etDerivedStateFromError的callback
- }, () => {
- // 用于執(zhí)行componentDidCatch的callback
- // 以及 用于拋出React提示信息的callback
- })
- 對(duì)于根節(jié)點(diǎn),執(zhí)行ReactDOM.render(element, container, callback)中callback參數(shù)能傳遞Function作為callback
所以,對(duì)于「沒(méi)有Error Boundaries」的情況,相當(dāng)于主動(dòng)執(zhí)行了如下函數(shù):
- ReactDOM.render(element, container, () => {
- // 用于拋出“未捕獲的錯(cuò)誤”及“React的提示信息”的callback
- })
所以,Error Boundaries的實(shí)現(xiàn)可以看作是:React利用已有API實(shí)現(xiàn)的新功能。
總結(jié)
經(jīng)常有人問(wèn):為什么Hooks沒(méi)有Error Boundaries?
可以看到,Error Boundaries的實(shí)現(xiàn)借助了this.setState可以傳遞callback的特性,useState暫時(shí)無(wú)法完全對(duì)標(biāo)。
最后,給你留個(gè)作業(yè),在官方文檔[1]介紹了4種情況的錯(cuò)誤不會(huì)被Error Boundaries捕獲。
利用本文知識(shí),你能分析下他們?yōu)槭裁床粫?huì)被捕獲么?
參考資料
[1]官方文檔:
https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries