일.코드 분할
프론트엔드 애플리케이션이 일정 규모에 도달했을 때 (예를 들어 bundle size 가 MB 단위), 반드시 코드 분할의 강한 수요에 직면합니다:
Code-Splitting is a feature supported by bundlers like Webpack and Browserify (via factor-bundle) which can create multiple bundles that can be dynamically loaded at runtime.
런타임에서 몇 가지 코드 블록을 동적으로 로드합니다. 예를 들어, 비첫 화면 비즈니스 컴포넌트, 및 캘린더, 주소 선택, 댓글 등의 중량급 컴포넌트
가장 편리한 동적 로드 방식은 아직 stage3 이지만, 이미 각打包툴 ( webpack, rollup 등) 에 널리 지원되는 tc39/proposal-dynamic-import 입니다:
import('../components/Hello').then(Hello => {
console.log(<Hello />);
});
(setTimeout 으로 비동기 로드 컴포넌트를 시뮬레이션) 에 상당:
new Promise(resolve =>
setTimeout(() =>
resolve({
// 来自另一个文件的函数式组件
default: function render() {
return <div>Hello</div>
}
}),
3000
)
).then(({ default: Hello }) => {
// 拿到组件了,然后呢?
console.log(<Hello />);
});
물론, 분할하는 것은 전반부일 뿐이고, 손에 넣은 컴포넌트를 어떻게 렌더링하는가 가 후반부입니다
이.조건 렌더링
프레임워크 지원에 의존하지 않는 경우, 조건 렌더링 방식으로 동적 컴포넌트를挂载할 수 있습니다:
class MyComponent extends Component {
constructor() {
super();
this.state = {};
// 动态加载
import('./OtherComponent').then(({ default: OtherComponent }) => {
this.setState({ OtherComponent });
});
}
render() {
const { OtherComponent } = this.state;
return (
<div>
{/* 条件渲染 */}
{ OtherComponent && <OtherComponent /> }
</div>
);
}
}
이 때의 대응하는 사용자 체험은, 첫 화면 OtherComponent 가 아직 돌아오지 않았고, 잠시 후 레이아웃이抖れて나타나며, 몇 가지 문제가 존재:
-
부모 컴포넌트에 침입성이 있음 (
state.OtherComponent) -
레이아웃抖动체험이 좋지 않음
프레임워크가 지원을 제공하지 않는 경우, 이 침입성은 피할 수 없는 것 같습니다 (항상 컴포넌트가 조건 렌더링을 수행해야 하고, 항상 이러한 표시 로직을 추가해야 함)
抖动의 경우, loading 을 추가하여 해결하지만, 遍地天窗 (여러 loading 이 모두 회전하고 있음) 라는 체험 문제가 발생하기 쉽습니다. 따라서 loading 은 일반적으로 단일 원자 컴포넌트를 대상으로 하지 않고, 컴포넌트 트리의 한块의 영역 전체에 loading 을 표시합니다 (이 영역에는 본능立即표시할 수 있는 컴포넌트가 포함되어 있을 수 있습니다). 이 시나리오에서는, loading 은 조상 컴포넌트에 추가해야 하고, 표시 로직이 매우 번거로워집니다 (여러 동적 컴포넌트가 모두 로드 완료될 때까지 숨기지 않아야 할 수 있습니다)
따라서, 조건 렌더링이 가져오는 침입성을 회피하고 싶다면, 프레임워크가 지원을 제공하는 것에 의지할 수밖에 없습니다. 이것이 바로React.lazy API 의 유래입니다. 그리고 뒤의 2 가지 문제를 해결하기 위해, loading 표시 로직을 조상 컴포넌트에 배치하기를 원합니다. 이것이Suspense 의 작용입니다
삼.React.lazy
React.lazy() 는 조건 렌더링 세부사항을 프레임워크 층으로 옮기고, 동적으로 도입된 컴포넌트를 일반 컴포넌트로 사용하는 것을 허용하며, 우아하게 이 침입성을消除:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
동적으로 도입된 OtherComponent 는 용법상 일반 컴포넌트와 완전히 일치하며, 도입 방식상의 차이만 존재합니다 (import 를 import() 로 변경하고 React.lazy() 로 감쌈):
import OtherComponent from './OtherComponent';
// 改为动态加载
const OtherComponent = React.lazy(() => import('./OtherComponent'));
import() 이 ES Module 을 resolve 하는 Promise 를 반환해야 하고, 이 ES Module 里에서 export default 가 합법적인 React 컴포넌트여야 합니다:
// ./OtherComponent.jsx
export default function render() {
return <div>Other Component</div>
}
유사:
const OtherComponent = React.lazy(() => new Promise(resolve =>
setTimeout(() =>
resolve(
// 模拟 ES Module
{
// 模拟 export default
default: function render() {
return <div>Other Component</div>
}
}
),
3000
)
));
P.S.React.lazy() 는暫時 SSR 을 지원하지 않습니다. React Loadable 을 사용할 것을 권장
사.Suspense
React.Suspense 도 가상 컴포넌트입니다 (Fragment 와 유사, 타입标识로서만 사용). 용법은 다음과 같습니다:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
Suspense 子樹중에 아직 돌아오지 않은 Lazy 컴포넌트가 존재하면, fallback 이 지정한 콘텐츠로 진행합니다. 이것이 바로 임의 조상급으로 提升할 수 있는 loading 이 아닙니까?
You can place the Suspense component anywhere above the lazy component. You can even wrap multiple lazy components with a single Suspense component.
Suspense 컴포넌트는 (컴포넌트 트리 중의) Lazy 컴포넌트上方的임의위치에 배치할 수 있고, 아래에 여러 Lazy 컴포넌트를 가질 수 있습니다. loading 시나리오에 대응하면, 이 2 가지 능력:
-
loading 提升을 지원
-
loading 聚合을 지원
4 줄의 비즈니스 코드로 loading 베스트 프랙티스를 실현할 수 있으며, 매우 아름다운 특성
P.S.Suspense 로 감싸지지 않은 Lazy 컴포넌트는 오류를 보고:
Uncaught Error: A React component suspended while rendering, but no fallback UI was specified.
프레임워크 층에서 사용자 체험에 강한 요구를 제출한 것으로算是
오.구체적 구현
function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
return {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// 组件加载状态
_status: -1,
// 加载结果,Component or Error
_result: null,
};
}
전달된 컴포넌트 로더를 기록하고, (로드) 상태付き의 Lazy 컴포넌트 기술 오브젝트를 반환:
// _status 取值
export const Pending = 0;
export const Resolved = 1;
export const Rejected = 2;
초기값 -1 은摸힌 후 Pending 으로 변화:
// beginWork()
// mountLazyComponent()
// readLazyComponentType()
function readLazyComponentType(lazyComponent) {
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
lazyComponent._result = thenable;
throw thenable;
}
마지막 throw 에 주의. 그렇습니다, 子樹렌더링을打断하기 위해, 여기서 직접 오류를 던집니다. 길이 조금狂野:
function renderRoot(root, isYieldy) {
do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
// 处理错误
throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime);
// 找到下一个工作单元,Lazy 父组件或兄弟组件
nextUnitOfWork = completeUnitOfWork(sourceFiber);
continue;
}
} while (true);
}
마지막으로長達 230 行의 throwException 으로兜:
function throwException() {
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// This is a thenable.
const thenable: Thenable = (value: any);
// 接下来大致做了 4 件事
// 1.找出祖先所有 Suspense 组件的最早超时时间(有可能已超时)
// 2.找到最近的 Suspense 组件,找不到的话报那个错
// 3.监听 Pending 组件,等到不 Pending 了立即调度渲染最近的 Suspense 组件
// Attach a listener to the promise to "ping" the root and retry.
let onResolveOrReject = retrySuspendedRoot.bind(
null,
root,
workInProgress,
sourceFiber,
pingTime,
);
if (enableSchedulerTracing) {
onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject);
}
thenable.then(onResolveOrReject, onResolveOrReject);
// 4.挂起最近的 Suspense 组件子树,不再往下渲染
}
}
P.S. 주의, 제 3 보 thenable.then(render, render) 는 React.lazy(() => resolvedImportPromise) 의 시나리오에서fallback 콘텐츠를閃하지 않습니다. 이는 브라우저 태스크 메커니즘과 관계. 자세한 내용은 macrotask 와 microtask 참조
(결과를 수집時) 최근의 Suspense 컴포넌트로 돌아와, Pending 후代가 발견되면 fallback 을 렌더링:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
if (
primaryChildExpirationTime !== NoWork &&
primaryChildExpirationTime >= renderExpirationTime
) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
}
}
function updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
) {
// 渲染 fallback
const nextFallbackChildren = nextProps.fallback;
const primaryChildFragment = createFiberFromFragment(
null,
mode,
NoWork,
null,
);
const fallbackChildFragment = createFiberFromFragment(
nextFallbackChildren,
mode,
renderExpirationTime,
null,
);
next = fallbackChildFragment;
return next;
}
이상, 거의 전 과정입니다 (생략할 수 있는 세부사항은 모두 생략)
육.의의
We've built a generic way for components to suspend rendering while they load async data, which we call suspense. You can pause any state update until the data is ready, and you can add async loading to any component deep in the tree without plumbing all the props and state through your app and hoisting the logic. On a fast network, updates appear very fluid and instantaneous without a jarring cascade of spinners that appear and disappear. On a slow network, you can intentionally design which loading states the user should see and how granular or coarse they should be, instead of showing spinners based on how the code is written. The app stays responsive throughout.
취지는 loading 시나리오에 우아한通用해결책을 제공하고, 컴포넌트 트리가 비동기 데이터를 기다려挂起 (즉 지연 렌더링) 하는 것을 허용. 의의는:
-
最佳사용자 체험에 부합:
-
레이아웃抖动을 회피 (데이터가 돌아온 후에 한块의 콘텐츠가 나타남). 물론, 이는 loading 또는 skeleton 을 추가하는 이점이고, Suspense 와는 관계가 그리 크지 않음
-
서로 다른 네트워크 환경을 구별하여 다룸 (데이터가 빨리 돌아오면 loading 은 나타나지 않음)
-
-
우아: 子樹 loading 을 추가하기 위해 상태와 로직을提升할 필요가 없어지고, 상태提升과 컴포넌트封鎖성의 우울에서解脱
-
유연: loading 컴포넌트와 비동기 컴포넌트 (비동기 데이터에 의존하는 컴포넌트) 사이에 컴포넌트階級관계상의 강한 연관이 없고, loading 粒度를 유연하게 제어 가능
-
通用:비동기 데이터를 기다릴 때降级컴포넌트를 표시하는 것을 지원 (loading 은 가장 일반적인降级전략의 일종으로, fallback 이 캐시 데이터 또는 광고라도不可以하지 않음)
아직 댓글이 없습니다