一.UI 레이어의 try...catch
먼저 결론을 말합니다. Suspense 는 try...catch 처럼, UI 가 안전한지 여부를 결정합니다:
try {
// 一旦有没 ready 的东西
} catch {
// 立即进入 catch 块,走 fallback
}
그렇다면, 어떻게 안전을 정의할까요?
생각해 보세요. 만약 컴포넌트의 코드가 아직 로드 완료되지 않았는데, 그것을 렌더링하려고 한다면, 분명히 안전하지 않습니다. 따라서, 좁은 의미로는컴포넌트 코드가 준비 완료된 컴포넌트는 안전하다고 생각합니다. 동기 컴포넌트와 로드 완료된 비동기 컴포넌트 (React.lazy) 를 포함합니다. 예를 들어:
// 同步组件,安全
import OtherComponent from './OtherComponent';
// 异步组件,不安全
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
// ...等到 AnotherComponent 代码加载完成之后
// 已加载完的异步组件,安全
AnotherComponent
Error Boundary
비슷한 것으로 Error Boundary 가 있습니다. 이것도 UI 레이어의 try...catch 의 일종으로, 그 안전의 정의는 컴포넌트 코드 실행에 JavaScript Error 가 없는 것입니다:
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
이 두 가지 정의는 충돌하지 않는다는 것을 알 수 있습니다. 실제로, Suspense 와 Error Boundary 는 확실히 공존할 수 있습니다. 예를 들어 Error Boundary 를 통해 비동기 컴포넌트 로드 오류를 캡처합니다:
If the other module fails to load (for example, due to network failure), it will trigger an error. You can handle these errors to show a nice user experience and manage recovery with Error Boundaries.
예를 들어:
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
二.Suspense 를 손으로 만들기
서두의 5 줄 코드는 흥미로울 수 있지만, 아직 충분히 명확하지 않습니다. 계속 채웁니다:
function Suspense(props) {
const { children, fallback } = props;
try {
// 一旦有没 ready 的东西
React.Children.forEach(children, function() {
assertReady(this);
});
} catch {
// 立即进入 catch 块,走 fallback
return fallback;
}
return children;
}
assertReady 는 어설션으로, 안전하지 않은 컴포넌트에 대해 Error 를 던집니다:
import { isLazy } from "react-is";
function assertReady(element) {
// 尚未加载完成的 Lazy 组件不安全
if (isLazy(element) && element.type._status !== 1) {
throw new Error('Not ready yet.');
}
}
P.S.react-is 는 Lazy 컴포넌트를 구분하는 데 사용되며, _status 는 Lazy 컴포넌트의 로드 상태를 나타냅니다. 상세는 React Suspense | 具体实现 참조
시연해 보세요:
function App() {
return (<>
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<ReadyLazyComponent />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
</>);
}
대응하는 인터페이스 내용은:
Hello, there.
loading...
ready lazy component.
loading...
첫 렌더링 결과는 예상대로입니다. 이후의 업데이트 프로세스 (컴포넌트 로드 완료 후 loading 을 실제 콘텐츠로 교체) 에 대해서는, 오히려 Lazy 컴포넌트의 렌더링 메커니즘의 범주에 속하며, Suspense 와는あまり 관계가 없습니다. 여기서는 전개하지 않습니다. 관심이 있으면 React Suspense | 具体实现 를 참조하세요
P.S.그 중에서, ReadyLazyComponent 의 구축에는 약간의 기술이 있습니다:
const ReadyLazyComponent = React.lazy(() =>
// 模拟 import('path/to/SomeOtherComponent.js')
Promise.resolve({
default: () => {
return <p>ready lazy component.</p>;
}
})
);
// 把 Lazy Component 渲染一次,触发其加载,使其 ready
const rootElement = document.getElementById("root");
// 仅用来预加载 lazy 组件,忽略缺少外层 Suspense 引发的 Warning
ReactDOM.createRoot(rootElement).render(<ReadyLazyComponent />);
setTimeout(() => {
// 等上面渲染完后,ReadyLazyComponent 就真正 ready 了
});
Lazy Component 는 정말 render 가 필요할 때에만 로드됩니다 (소위 lazy). 따라서, 먼저 1 회 렌더링하고, 그 후 다시 사용할 때 ready 가 됩니다
三.try...catch 와의 유추
위와 같이, Suspense 와 try...catch 의 대응 관계는:
-
Suspense:
try에 대응 -
fallback:
catch에 대응 -
尚未加载完成的 Lazy Component:
Error에 대응
원리상의 유사성으로 인해, Suspense 의 많은 특징은 try...catch 와의 유추를 통해 쉽게 이해할 수 있습니다. 예를 들어:
-
就近 fallback:
Error가 던져진 후 위로 올라가 가장 가까운try에 대응하는catch를 찾음 -
存在未 ready 组件就 fallback: 一大块
try中,只要有一个Error就立即进入catch
따라서, Suspense 로 감싸진 한 세트의 컴포넌트에 대해,要么全都展示出来(包括可能含有的 fallback 内容)、要么全都不展示(转而展示该 Suspense 的 fallback). 이 점을 이해하는 것은 Suspense 를 습득하는 데 특히 중요합니다
성능 영향
前面示例中的처럼:
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
렌더링 결과는 loading... 입니다. LazyComponent 를 처리할 때 Suspense fallback 이 트리거되므로, 이미 처리 완료된 Hello, there. 도, 尚未处理到的 ReadyLazyComponent 도 표시할 수 없습니다. 따라서, 3 가지 문제가 존재합니다:
-
伤及池鱼:一个尚未加载完成的 Lazy Component 就能让它前面许多本能立即显示的组件无法显示
-
阻塞渲染:尚未加载完成的 Lazy Component 会阻断渲染流程,阻塞最近 Suspense 祖先下其后所有组件的渲染,造成串行等待
따라서, try...catch 를 사용하는 것과 마찬가지로, Suspense 를 남용하는 것도 (UI 레이어의) 성능에 영향을 미칩니다. 기술적으로는 애플리케이션 전체를 최상위 Suspense 에 감싸는 것으로 확실히 모든 Lazy Component 에 fallback 을 제공할 수 있지만:
<Suspense fallback={<div>global loading...</div>}>
<App />
</Suspense>
하지만, 이렇게 하는 결과를 명확히 인식해야 합니다
구조 특징
Suspense 는 try...catch 와 마찬가지로, 고정 구조를 제공함으로써 조건 판단을 제거합니다:
try {
// 如果出现 Error
} catch {
// 则进入 catch
}
분기 로직을 구문 구조에 고정했습니다. Suspense 도 유사합니다:
<Suspense fallback={ /* 则进入 fallback */ }>
{ /* 如果出现未 ready 的 Lazy 组件 */ }
</Suspense>
이렇게 하는 이점은 코드 중에 조건 판단이 나타나지 않아도 된다는 것입니다. 따라서局部状态에 의존하지 않으며, 우리는 그 작용 범위를 쉽게 조정할 수 있습니다:
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
변경하여:
<p>Hello, there.</p>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
<ReadyLazyComponent />
전후로 거의 변경 비용이 없습니다. try...catch 경계를 조정하는 것보다 더 쉽습니다 (변수의 작용역을 고려할 필요가 없으므로). 이는loading 의 입자도, 순서를 무伤으로 조정하는 데 매우 의미가 있습니다:
Suspense lets us change the granularity of our loading states and orchestrate their sequencing without invasive changes to our code.
四.온라인 Demo
文中涉及的所以重要示例,都在 Demo 프로젝트中(含详尽注释):
五.정리
Suspense 는 UI 레이어의 try...catch 와 같은 것이지만, 그 캡처하는 것은 예외가 아니라, 尚未加载完成的 컴포넌트입니다
물론, Error Boundary 도 마찬가지이며, 둘은 각 catch 各的互不冲突
아직 댓글이 없습니다