kangyana / daily-question

When your heart is set on something, you get closer to your goal with each passing day.
https://www.webpack.top
MIT License
3 stars 0 forks source link

【Q075】Suspense #75

Open kangyana opened 2 years ago

kangyana commented 2 years ago

suspense,lazy

kangyana commented 2 years ago

1. React.Suspense

Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。 目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

请注意,lazy 组件可以位于 Suspense 组件树的深处——它不必包装树中的每一个延迟加载组件。 最佳实践是将 <Suspense> 置于你想展示加载指示器(loading indicator)的位置, 而 lazy() 则可被放置于任何你想要做代码分割的地方。

服务端渲染中的 Suspense

在服务端渲染过程中,Suspense 边界允许你挂起,通过较小的块来刷新应用程序。 当组件挂起时,我们会安排一个低优先级的任务来渲染最近的 Suspense 边界的 fallback。 如果组件在我们刷新 fallback 之前取消挂起,那么我们会发送实际内容并丢弃 fallback

hydrate 过程中的 Suspense

Suspense 边界依赖于它们的父边界,在它们可以 hydrate 前被 hydrate, 但是它们可以独立于兄弟边界进行 hydrate。 边界 hydrate 前发生的事件将导致边界 hydrate 的优先级高于相邻边界的优先级。

2. React.lazy

lazy 函数能让你像渲染常规组件一样处理动态引入。

import OtherComponent from './OtherComponent';
// 改为动态引入
const OtherComponent = React.lazy(() => import('./OtherComponent'));

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

lazy 接受一个函数,这个函数需要动态调用 import()。 它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

应在 Suspense 组件中渲染 lazy 组件, 如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。 你可以将 Suspense 组件置于懒加载组件之上的任何位置。 你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}

避免兜底

任何组件都可能因渲染而暂停,甚至是已经展示给用户的组件。 为了使屏幕内容始终一致,如果一个已经显示的组件暂停,React 必须隐藏它的树,直到最近的 <Suspense> 边界。 然而,从用户的角度来看,这可能会使人很困惑。

参考这个标签切换的示例:

import React, { Suspense } from 'react';
import Tabs from './Tabs';
import Glimmer from './Glimmer';

const Comments = React.lazy(() => import('./Comments'));
const Photos = React.lazy(() => import('./Photos'));

function MyComponent() {
  const [tab, setTab] = React.useState('photos');

  function handleTabSelect(tab) {
    setTab(tab);
  };

  return (
    <div>
      <Tabs onTabSelect={handleTabSelect} />
      <Suspense fallback={<Glimmer />}>
        {tab === 'photos' ? <Photos /> : <Comments />}
      </Suspense>
    </div>
  );
}

在这个示例中,如果标签从 photos 切换为 comments,但 Comments 会暂停,用户会看到屏幕闪烁。 这符合常理,因为用户不想看到 photos,而 Comments 组件还没有准备好渲染其内容, 而 React 为了保证用户体验的一致性,只能显示上面的 Glimmer,别无选择。

然而,有时这种用户体验并不可取。特别是在准备新 UI 时,展示 “旧” 的 UI 会体验更好。 你可以尝试使用新的 startTransition API 来让 React 实现这一点:

function handleTabSelect(tab) {
  startTransition(() => {
    setTab(tab);
  });
}

此处代码会告知 React,将标签切换为 comments 不会标记为紧急更新,而是标记为需要一些准备时间的 transition。 然后 React 会保留旧的 UI 并进行交互,当它准备好时,会切换为 <Comments />

异常捕获边界(Error boundaries)

如果模块加载失败(如网络问题),它会触发一个错误。 你可以通过 异常捕获边界(Error boundaries) 技术来处理这些情况,以显示良好的用户体验并管理恢复事宜。

import React, { Suspense } from 'react';
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>
);