开始之前,我先问大家一个问题,你写 React
代码时,是否使用过 throw
关键字?
ErrorBoundary
大家好,今天我们来聊聊为啥我们写 React
代码时一定要使用 throw
关键字。
首先想到的是,React
中存在异常边界的概念。
异常边界是指一种用于捕获子组件树中运行时
错误的机制。当程序发生异常时,不会显示白屏,而是在局部显示异常错误提示。
❝注意,是运行时阶段,也就是
render
阶段,副作用中的异常还是需要手动捕获的。
它的实现原理是使用类组件的生命周期方法:static getDerivedStateFromError(error)
import React from"react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 更新状态以渲染备用 UI
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
// 渲染备用 UI
returnthis.props.fallback || <h1>Something went wrong.</h1>;
}
// 正常渲染子组件
returnthis.props.children;
}
}
exportdefault ErrorBoundary;
使用时:
function FaultyComponent() {
const num = 10
// number 类型没有 concat 方法,这里调用会报错
return<>{num.concat()}</>
}
function OtherComponent() {
thrownewError("This component always crashes!");
}
function App() {
return (
<Layout>
<Header>Header</Header>
<Content>
<ErrorBoundary>
<FaultyComponent />
<OtherComponent />
</ErrorBoundary>
</Content>
</Layout>
);
}
下面的异常无法捕获,因为是事件处理程序中的异常。
function handleClick() {
try {
throw new Error("Event error!");
} catch (error) {
// 这种写法也是很有必要的
console.error(error);
}
}
Suspense
与 请求前置
还能想到的一种情况,就是当我们第一次加载页面并发送请求时。相信大家一定遇到过这种情况,加载一个页面,首先看到的是页面加载种,然后会出现一个空表格,也是加载种,这种用户体验,我愿称之为糟糕。
罪魁祸首是对 Suspense
的理解不到位。写出了如下的代码。
function Dashboard() {
const {data, loading, error} = useRequest(getDashBoardData)
if (loading) {
return <div>dashboard laoding ....</div>
}
return <div>dashboard: {data}</div>
}
function App() {
return (
<ErrorBoundary fallback={<div>Custom error fallback!</div>}>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path={'/'} exact><Redirect to={'/dashboard'}></Redirect></Route>
<Route component={Dashboard} path={`/dashboard`} exact></Route>
<Route component={Other} path={`/other`} exact></Route>
</Switch>
</Suspense>
</ErrorBoundary>
);
}
这种代码的逻辑主要是先加载页面,然后加载数据。
graph LR
请求页面资源 --> 加载数据 --> 显示结果
实际上我们可以借助 Suspense
的让请求资源和加载数据同时进行。
graph LR
请求页面资源 --> 显示结果
加载数据 --> 显示结果
Suspense
组件是 React
提供的一种处理异步状态的机制。它可以捕获被包裹组件中的异步行为。并展示为预定的加载中状态,例如:import
引入的组件加载时,或者组件中的数据处于异步中时。
这里还需要注意,这种组件中的异步是需要子组件在渲染(render
)阶阶段抛出一个Promise
。
当我们发送请求时,Promise
处于加载中状态,Suspense
可以识别到加载状态从而显示加载页面。而当Promise
处于非加载状态时,则显示结果页面。
function wrapPromise(promise) {
let status = "pending";
let result;
const suspender = promise.then(
(res) => {
status = "success";
result = res;
},
(err) => {
status = "error";
result = err;
}
);
return {
read() {
if (status === "pending") {
throw suspender; // 抛出 Promise
} elseif (status === "error") {
throw result; // 抛出错误,被 ErrorBoundary 捕获
} else {
return result; // 返回结果
}
},
};
}
const resource = wrapPromise(fetchData());
function Dashboard() {
const data = resource.read();
return <div>dashboard: {data}</div>;
}
这么写有很多好处,首先是请求前置了,可以减少首屏加载的时间。
其次,可以将 UI
和业务抽离。不管后续业务逻辑怎么更迭,UI
都无需更改。甚至可以原封不动的运行在Web
、React Native
、SSR
。这或许就是React
设计哲学的魅力之处了!
但是需要注意的是,如果将请求放入交互或者副作用函数中,则无法捕获异步状态了,因为此时已经不是 render
阶段了
function Dashboard() {
let data = null;
useEffect(() => {
data = resource.read();
}. [])
return <div>dashboard: {data}</div>;
}
function Dashboard() {
let data = null;
const getData = () => {
data = resource.read();
}
return <div>dashboard: {data} <button onClick={getData}>请求</button></div>;
}
好啦,今天的分享就这么多了,希望可以帮助到你,如果文章有错误的地方欢迎指正!