React Suspense

栏目: 服务器 · 发布时间: 1年前

来源: www.ayqy.net

本文转载自:http://www.ayqy.net/blog/react-suspense/,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有。

一.代码拆分

前端应用达到一定规模时(比如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,但已经被各大打包工具( webpackrollup 等)广泛支持的 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的由来。而为了解决后两个问题,我们希望把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() 必须返回一个会 resolve ES Module的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场景,就是这两种能力:

  • 支持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.

初衷是为logading场景提供优雅的通用解决方案,允许组件树挂起等待(即延迟渲染)异步数据,意义在于:

  • 符合最佳用户体验:

    • 避免布局抖动(数据回来之后冒出来一块内容),当然,这是加loading或skeleton的好处,与Suspense关系不很大

    • 区别对待不同网络环境(数据返回快的话压根不会出现loading)

  • 优雅:不用再为了加子树loading而提升相关状态和逻辑,从状态提升与组件封装性的抑郁中解脱了

  • 灵活:loading组件与异步组件(依赖异步数据的组件)之间没有组件层级关系上的强关联,能够灵活控制loading粒度

  • 通用:支持等待异步数据时显示降级组件(loading只是一种最常见的降级策略,fallback到缓存数据甚至广告也不是不可以)

参考资料


以上所述就是小编给大家介绍的《React Suspense》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

关注码农网公众号

关注我们,获取更多IT资讯^_^


为你推荐:

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入React技术栈

深入React技术栈

陈屹 / 人民邮电出版社 / 2016-11-1 / CNY 79.00

全面讲述React技术栈的第一本原创图书,pure render专栏主创倾力打造 覆盖React、Flux、Redux及可视化,帮助开发者在实践中深入理解技术和源码 前端组件化主流解决方案,一本书玩转React“全家桶” 本书讲解了非常多的内容,不仅介绍了面向普通用户的API、应用架构和周边工具,还深入介绍了底层实现。此外,本书非常重视实战,每一节都有实际的例子,细节丰富。我从这......一起来看看 《深入React技术栈》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具