React.Suspense是什么

19,369 阅读5分钟

suspense的字面意思就是悬而不决,用在平时开发中,就可以理解为还没有完成的事,你不知道啥时候完成。也就是异步,异步加载组件,异步请求数据。

Suspense是用来做什么的?

1. 代码拆分

服务于打包优化的代码拆分。lazysuspense配合使用

const A = React.lazy(() => import('./A'))

return (
  <Suspense fallback={<p>loading</p>}>
    <Route component={A} path="/a">
  </Suspense>
)

这样在打包代码时,可以显著减少主包的体积,加快加载速度,从而提升用户体验;而当路由切换时,加载新的组件代码,代码加载是异步的过程,此时suspense就会进如fallback,那我们看到的就是loading,显式的告诉用户正在加载,当代码加载完成就会展示A组件的内容,整个loading状态不用开发者去控制。

2. 异步加载数据

既然代码拆分可以不用维护loading,那么加载数据是不是也可以不用维护loading状态呢?

当然可以!

但是直接用axios或者fetch是无法进入suspense的fallback的,但是react提供了一个库供我们使用react-cache(暂不建议使用的),它具体是做什么的,原理是什么,我们后面在讨论,这里先体验一下效果如何。

  • 现在用的方法,管理loading状态
function A() {
  const [loading, setLoading] = useState(false)
  const [list, setList] = useState([])
  
  function getList() {
    setloading(true)
    promise.then(res => {
      setList(res)
    })
    .finally(() => {
      setLoading(false)
    })
  }
  
  useEffect(() => {
    getList()
  }, [])
  
  return (
    <>
      data...
      {loading && <p>loading</p>}
    </>
  )
}
  • 使用suspense处理数据加载
const resource = unstable_createResource((id) => {
  return Promise.resolve([])
})

function B() {
  const data = resource.read(0)
  
  return (
    <Suspense fallback={<p>loading</p}>
      data...
    </Suspense>
  )
}

可以看到,使用suspense的方式,在开发的时候完全不用维护loading状态,而且还有一个比较大的差别,B中的list是没有使用state的,它获取的点是在B渲染时,而A获取数据则是发生在A渲染后

细谈Suspense

1. 使用方式(触发方式)

  • react-cache

为什么直接使用axios或者fetch不能进入suspense的fallback呢,而使用react-cache包一下请求,就可以进入fallback了呢?

来康康react-cache的源码

read: function (input) {
      // react-cache currently doesn't rely on context, but it may in the
      // future, so we read anyway to prevent access outside of render.
      readContext(CacheContext);
      var key = hashInput(input);
      var result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        case Pending:
          {
            var suspender = result.value;
            throw suspender;
          }
        case Resolved:
          {
            var _value = result.value;
            return _value;
          }
        case Rejected:
          {
            var error = result.value;
            throw error;
          }
        default:
          // Should be unreachable
          return undefined;
      }
    },

这里我截取了部分源码,可以看到这是一个类似promise的结构,有三种状态,而需要注意的是pending状态。

首先看看这个result是这个什么东西,可以理解为result.value就是传入的数据请求方法,执行返回的promise或者说likePromise,当这个promise未决时,会throw出这个promise,而此时suspense看到这个promise自然就知道还处于数据请求中,就会展示fallback中的内容,当这个promise已决时,则代表数据请求结束,suspense就应该展示数据内容。

大致的suspense工作的流程就是:

1. 事先throw
2. 在 completeWork 之前 catch 住
3. 然后添加到 updateQueue 里
4. updateQueue 批量更新

这也就解释了为什么直接使用axios、fetch无法展示suspense的fallback。

在理解原理之后,你可以很轻易的写出类似于这样的代码,替代一下react-cache

function promiseWrapper(promise) {
  let status = "pending";
  let result;
  let likePromise = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw likePromise;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}
  • Relay
  • swr

2. 组件渲染顺序

  • 挂载后渲染
  • 获取数据后渲染
  • 并行获取数据和渲染组件

上面说到组件的渲染顺序,在没使用suspense时,数据获取是在组件渲染完成之后进行的。而使用suspense的方式,组件是如何渲染的?

// 官方示例
const resource = http()

function Foo () {
  const result = resource.foo.read()
  return <div>Foo: { result.name }</div>
}

function Bar () {
  const result = resource.bar.read()
  return <div>Bar: { result.name }</div>
}

function Page () {
  return (
    <React.Suspense fallback={<h1>Loading Foo...</h1>}>
      <Foo/>
      <React.Suspense fallback={<h1>Loading Bar...</h1>}>
        <Bar/>
      </React.Suspense>
    </React.Suspense>
  )
}

3. race condition

React 组件有它们自己的“生命周期”。组件可能在任意时间点接收到 props 或者更新 state。然而,每一个异步请求同样也有自己的“生命周期”。异步请求的生命周期开始于我们发出请求,结束于我们收到响应报文。

4. 错误边界

由于以上请求的调用方式,们不等待promise就直接开始渲染,使得我们不太好使用promise.catch去捕获异步中的错误,所以得另辟蹊径

getDerivedStateFromError生命周期函数允许我们捕获组件中出现的错误情况,所以我们可以很方便的写出一个类似这样的ErrorBoundary处理组件,去处理下面层级中出现的错误情况。

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error
    };
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

总结

说了那么多,Suspense 到底有什么用呢?对于这个问题,可以从不同的角度来回答:

  1. 它能让数据获取库与 React 紧密整合。如果一个数据请求库实现了对 Suspense 的支持,那么,在 React 中使用 Suspense 将会是自然不过的事。
  2. 它能让你有针对性地安排加载状态的展示。虽然它不干涉数据怎样获取,但它可以让你对应用的视图加载顺序有更大的控制权。
  3. 它能够消除 race conditions。即便是用上 await,异步代码还是很容易出错。相比之下,Suspense 更给人同步读取数据的感觉 —— 假定数据已经加载完毕。

总之,我更认为Suspense是一种改变你开发模式的,更好的UI解耦的一种模式。