大家好,我卡頌。
在很多全面使用Hooks
開發的團隊,唯一使用ClassComponent
的場景就是使用ClassComponent建立ErrorBoundary。
可以說,如果Hooks
存在如下兩個生命週期函式的替代品,就能全面拋棄ClassComponent
了:
- getDerivedStateFromError
- componentDidCatch
那為什麼還沒有對標的Hook
呢?
今天我們從上述兩個生命週期函式的實現原理,以及要移植到Hook
上需要付出的成本來談論這個問題。
歡迎加入人類高質量前端框架群,帶飛
ErrorBoundary實現原理
ErrorBoundary
可以捕獲子孫元件中React工作流程內的錯誤。
React工作流程指:
- render階段,即元件render、Diff演算法發生的階段
- commit階段,即渲染DOM、componentDidMount/Update執行的階段
這也是為什麼事件回撥中發生的錯誤無法被ErrorBoundary
捕獲 —— 事件回撥並不屬於React工作流程。
如何捕獲錯誤
render階段的整體執行流程如下:
do {
try {
// render階段具體的執行流程
workLoop();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
可以發現,如果render階段發生錯誤,會被捕獲並執行handleError
方法。
類似的,commit階段的整體執行流程如下:
try {
// ...具體執行流程
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
如果commit階段發生錯誤,會被捕獲並執行captureCommitPhaseError
方法。
getDerivedStateFromError原理
捕獲後的錯誤如何處理呢?
我們知道,ClassComponent
中this.setState
第一個引數,除了可以接收新的狀態,也能接收改變狀態的函式作為引數:
// 可以這樣
this.setState(this.state.num + 1)
// 也可以這樣
this.setState(num => num + 1)
getDerivedStateFromError
的實現,就藉助了this.setState
中改變狀態的函式這一特性。
當捕獲錯誤後,即:
- 對於render階段,
handleError
執行後 - 對於commit階段,
captureCommitPhaseError
執行後
會在ErrorBoundary
對應元件中觸發類似如下更新:
this.setState(
getDerivedStateFromError.bind(null, error)
)
這就是為什麼getDerivedStateFromError
要求開發者返回新的state —— 本質來說,他就是觸發一次新的更新。
componentDidCatch原理
再來看另一個ErrorBoundary
相關的生命週期函式 —— componentDidCatch
。
ClassComponent
中this.setState
的第二個引數,可以接收回撥函式作為引數:
this.setState(newState, () => {
// ...回撥
})
當觸發的更新渲染到頁面後,回撥會觸發。
這就是componentDidCatch
的實現原理。
當捕獲錯誤後,會在ErrorBoundary
對應元件中觸發類似如下更新:
this.setState(this.state, componentDidCatch.bind(this, error))
處理“未捕獲”的錯誤
可以發現,React執行流程中的錯誤,都已經被React
自身捕獲了,再交由ErrorBoundary
處理。
如果沒有定義ErrorBoundary
,這些被捕獲的錯誤需要重新丟擲,營造錯誤未被捕獲的感覺。
那這一步在哪裡執行呢?
與this.setState
類似,ReactDOM.render(element, container[, callback])
第三個引數也能接收回撥函式。
如果開發者沒有定義ErrorBoundary
,那麼React
最終會在ReactDOM.render
的回撥中丟擲錯誤。
可以發現,在ClassComponent
中ErrorBoundary
的實現完全依賴了ClassComponent
已有的特性。
而Hooks
本身並不存在類似this.setState
的回撥特性,所以實現起來會比較複雜。
實現Hooks中的ErrorBoundary
除了上述談到的阻礙,FunctionComponent
與ClassComponent
在原始碼層面的執行流程也有細節上的差異,要照搬實現也有一定難度。
如果一定要實現,在最大程度複用現有基礎設施的指導方針下,useErrorBoundary
(ErrorBoundary
在Hooks
中的實現)的使用方式應該類似如下:
function ErrorBoundary({children}: {children: ReactNode}) {
const [errorMsg, updateError] = useState<Error | null>(null);
useErrorBoundary((e: Error) => {
// 捕獲到錯誤,觸發更新
updateError(e);
})
return (
<div>
{errorMsg ? '報錯:' + errorMsg.toString() : children}
</div>
)
}
其中useErrorBoundary
的觸發方式類似useEffect
:
useErrorBoundary((e: Error) => {
// ...
})
// 類似
useEffect(() => {
// ...
})
筆者仿照ClassComponent
中ErrorBoundary
的實現原理與useEffect
的實現原理,實現了原生Hooks —— useErrorBoundary
。
感興趣的朋友可以在useErrorBoundary線上示例體驗效果。
總結
ErrorBoundary
在ClassComponent
中的實現使用了this.setState
的回撥函式特性,這使得Hooks
中要完全實現同樣功能,需要額外開發成本。
筆者猜測,這是沒有提供對應原生Hooks
的原因之一。