React 之 Suspense

简介: React 之 Suspense

前言


在上一篇《React 之 Race Condition》中,我们最后引入了 Suspense 来解决竞态条件问题,本篇我们来详细讲解一下 Suspense。


Suspense


React 16.6 新增了 组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面(像是个 spinner),让它在用户等待的时候显示。


目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件

const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
// 在 ProfilePage 组件处于加载阶段时显示一个 spinner
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>


执行机制


但这并不意味着 Suspense 不可以单独使用,我们可以写个 Suspense 单独使用的例子,不过目前使用起来会有些麻烦,但相信 React 官方会持续优化这个 API。

let data, promise;
function fetchData() {
  if (data) return data;
  promise = new Promise(resolve => {
    setTimeout(() => {
      data = 'data fetched'
      resolve()
    }, 3000)
  })
  throw promise;
}
function Content() {
  const data = fetchData();
  return <p>{data}</p>
}
function App() {
  return (
    <Suspense fallback={'loading data'}>
      <Content />
    </Suspense>
  )
}

这是一个非常简单的使用示例,但却可以用来解释 Suspense 的执行机制。


最一开始<Content> 组件会 throw 一个 promise,React 会捕获这个异常,发现是 promise 后,会在这个 promise 上追加一个 then 函数,在 then 函数中执行 Suspense 组件的更新,然后展示 fallback 内容。


等 fetchData 中的 promise resolve 后,会执行追加的 then 函数,触发 Suspense 组件的更新,此时有了 data 数据,因为没有异常,React 会删除 fallback 组件,正常展示  <Content />组件。


实际应用


如果我们每个请求都这样去写,代码会很冗余,虽然有 react-cache 这个 npm 包,但上次更新已经是 4 年之前了,不过通过查看包源码以及参考 React 官方的示例代码,在实际项目中,我们可以这样去写:

// 1. 通用的 wrapPromise 函数
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}
// 这里我们模拟了请求过程
const fakeFetch = () => {
  return new Promise(res => {
    setTimeout(() => res('data fetched'), 3000);
  });
};
// 2. 在渲染前发起请求
const resource = wrapPromise(fakeFetch());
function Content() {
  // 3. 通过 resource.read() 获取接口返回结果
  const data = resource.read();
  return <p>{data}</p>
}
function App() {
  return (
    <Suspense fallback={'loading data'}>
      <Content />
    </Suspense>
  )
}

在这段代码里,我们声明了一个 wrapPromise 函数,它接收一个 promise,比如 fetch 请求。函数返回一个带有 read 方法的对象,这是因为封装成方法后,代码可以延迟执行,我们就可以在 Suspense 组件更新的时候再执行方法,从而获取最新的返回结果。


函数内部记录了三种状态,pendingsuccesserror,根据状态返回不同的内容。


你可能会想,如果我们还要根据 id 之类的数据点击请求数据呢?使用 Suspense 该怎么做呢?React 官方文档也给了示例代码:

const fakeFetch = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`${id} data fetched`), 3000);
  });
};
// 1. 依然是直接请求数据
const initialResource = wrapPromise(fakeFetch(1));
function Content({resource}) {
  // 3. 通过 resource.read() 获取接口返回结果
  const data = resource.read();
  return <p>{data}</p>
}
function App() {
  // 2. 将 wrapPromise 返回的对象作为 props 传递给组件
  const [resource, setResource] = useState(initialResource);
  // 4. 重新请求
  const handleClick = (id) => () => {
    setResource(wrapPromise(fakeFetch(id)));
  }
  return (
    <Fragment>
      <button onClick={handleClick(1)}>tab 1</button>
      <button onClick={handleClick(2)}>tab 2</button>
      <Suspense fallback={'loading data'}>
        <Content resource={resource} />
      </Suspense>
    </Fragment>
  )
}


好处:请求前置


使用 Suspense 一个非常大的好处就是请求是一开始就执行的。回想过往的发送请求的时机,我们都是在 compentDidMount 的时候再请求的,React 是先渲染的节点再发送的请求,然而使用 Suspense,我们是先发送请求再渲染的节点,这就带来了体验上的提升。


尤其当请求多个接口的时候,借助 Suspense,我们可以实现接口并行处理以及提早展现,举个例子:

function fetchData(id) {
  return {
    user: wrapPromise(fakeFetchUser(id)),
    posts: wrapPromise(fakeFetchPosts(id))
  };
}
const fakeFetchUser = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`user ${id} data fetched`), 5000 * Math.random());
  });
};
const fakeFetchPosts = (id) => {
  return new Promise(res => {
    setTimeout(() => res(`posts ${id} data fetched`), 5000 * Math.random());
  });
};
const initialResource = fetchData(1);
function User({resource}) {
  const data = resource.user.read();
  return <p>{data}</p>
}
function Posts({resource}) {
  const data = resource.posts.read();
  return <p>{data}</p>
}
function App() {
  const [resource, setResource] = useState(initialResource);
  const handleClick = (id) => () => {
    setResource(fetchData(id));
  }
  return (
    <Fragment>
      <p><button onClick={handleClick(Math.ceil(Math.random() * 10))}>next user</button></p>
      <Suspense fallback={'loading user'}>
        <User resource={resource} />
        <Suspense fallback={'loading posts'}>
          <Posts resource={resource} />
        </Suspense>
      </Suspense>
    </Fragment>
  )
}

在这个示例代码中,user 和 posts 接口是并行请求的,如果 posts 接口提前返回,而 user 接口还未返回,会等到 user 接口返回后,再一起展现,但如果 user 接口提前返回,posts 接口后返回,则会先展示 user 信息,然后显示 loading posts,等 posts 接口返回,再展示 posts 内容。


image.png

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/586078a79a1a4b5a9378c9e99c49e4aa~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp


这听起来好像没什么,但是想想如果我们是以前会怎么做,我们可能会用一个 Promise.all 来实现,但是 Promise.all 的问题就在于必须等待所有接口返回才会执行,而且如果其中有一个 reject 了,都会走向 catch 逻辑。使用 Suspense,我们可以做到更好的展示效果。


好处:解决竞态条件


使用 Suspense 可以有效的解决 Race Conditions(竞态条件) 的问题,关于 Race Conditions 可以参考《React 之 Race Condition》


Suspense 之所以能够有效的解决 Race Conditions 问题,就在于传统的实现中,我们需要考虑 setState 的正确时机,执行顺序是:1. 请求数据 2. 数据返回 3. setState 数据


而在 Suspense 中,我们请求后,立刻就设置了 setState,然后就只用等待请求返回,React 执行 Suspense 的再次更新就好了,执行顺序是:1. 请求数据 2. setState 数据 3. 数据返回 4. Suspense 重新渲染,所以大大降低了出错的概率。

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
function fetchData(userId) {
  return wrapPromise(fakeFetch(userId))
}
const initialResource = fetchData('Nick');
function User({ resource }) {
  const data = resource.read();
  return <p>{ data }</p>
}
const App = () => {
  const [person, setPerson] = useState('Nick');
  const [resource, setResource] = useState(initialResource);
  const handleClick = (name) => () => {
    setPerson(name)
    setResource(fetchData(name));
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
      <button onClick={handleClick('Joe')}>Joe's Profile</button>
      <Fragment>
        <h1>{person}</h1>
        <Suspense fallback={'loading'}>
          <User resource={resource} />
        </Suspense>
      </Fragment>
    </Fragment>
  );
};


错误处理


注意我们使用的 wrapPromise 函数:

function wrapPromise(promise) {
  // ...
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

当 status 为 error 的时候,会 throw result 出来,如果 throw 是一个 promise,React 可以处理,但如果只是一个 error,React 就处理不了了,这就会导致渲染出现问题,所以我们有必要针对 status 为 error 的情况进行处理,React 官方文档也提供了方法,那就是定义一个错误边界组件:

// 定义一个错误边界组件
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;
  }
}
function App() {
  // ...
  return (
    <Fragment>
      <button onClick={handleClick(1)}>tab 1</button>
      <button onClick={handleClick(2)}>tab 2</button>
      <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
        <Suspense fallback={'loading data'}>
          <Content resource={resource} />
        </Suspense>
      </ErrorBoundary>
    </Fragment>
  )
}

<Content /> 组件 throw 出 error 的时候,就会被 <ErrorBoundary />组件捕获,然后展示 fallback 的内容。


源码


那 Suspense 的源码呢?我们查看 React.js 的源码

import {
  REACT_SUSPENSE_TYPE
} from 'shared/ReactSymbols';
export {
  REACT_SUSPENSE_TYPE as Suspense
};

再看下shared/ReactSymbols的源码

export const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense');

所以当我们写一个 Suspense 组件的时候:

<Suspense fallback={'loading data'}>
  <Content />
</Suspense>
// 被转译为
React.createElement(Suspense, {
  fallback: 'loading data'
}, React.createElement(Content, null));

createElement 传入的 Suspense 就只是一个常量而已,具体的处理逻辑会在以后的文章中慢慢讲解。

目录
相关文章
|
3月前
|
前端开发 JavaScript
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
本文介绍了在React项目中实现路由懒加载的方法,使用React提供的`lazy`和`Suspense`来优化项目首次加载的速度。通过将路由组件改为懒加载的方式,可以显著减少初始包的大小,从而加快首次加载速度。文章还展示了如何使用`Suspense`组件包裹`Switch`来实现懒加载过程中的fallback效果,并提供了使用前后的加载时间对比,说明了懒加载对性能的提升作用。
246 2
React项目路由懒加载lazy、Suspense,使第一次打开项目页面变快
|
2月前
|
缓存 前端开发 UED
React Suspense 懒加载详解
【10月更文挑战第18天】React Suspense 是 React 16.6 引入的新特性,主要用于处理异步数据获取和组件懒加载。本文从 Suspense 的基本概念出发,介绍了其在代码分割和数据获取中的应用,通过具体代码示例展示了如何使用 `React.lazy` 和 `Suspense` 实现组件的懒加载,并探讨了实践中常见的问题及解决方法,帮助开发者提升应用性能和用户体验。
111 1
|
4月前
|
资源调度 前端开发 API
React Suspense与Concurrent Mode:异步渲染的未来
React的Suspense与Concurrent Mode是16.8版后引入的功能,旨在改善用户体验与性能。Suspense组件作为异步边界,允许子组件在数据加载完成前显示占位符,结合React.lazy实现懒加载,优化资源调度。Concurrent Mode则通过并发渲染与智能调度提升应用响应性,支持时间分片和优先级调度,确保即使处理复杂任务时UI仍流畅。二者结合使用,能显著提高应用效率与交互体验,尤其适用于数据驱动的应用场景。
78 20
|
4月前
|
前端开发 UED
React Suspense 大揭秘!异步加载与优雅降级的神奇黑科技,让你的 React 应用更出色!
【8月更文挑战第31天】React Suspense 是 React 提供的一种处理异步数据加载和优雅降级的特性。它通过 `React.lazy` 和 `Suspense` 组件实现异步加载,在加载过程中显示提示信息,并通过错误边界组件 `Error Boundary` 捕获错误,避免应用崩溃,从而提升用户体验。
102 0
|
4月前
|
前端开发 UED 开发者
React.lazy()与Suspense:实现按需加载的动态组件——深入理解代码分割、提升首屏速度和优化用户体验的关键技术
【8月更文挑战第31天】在现代Web应用中,性能优化至关重要,特别是减少首屏加载时间和提升用户交互体验。React.lazy()和Suspense组件提供了一种优雅的解决方案,允许按需加载组件,仅在需要渲染时加载相应代码块,从而加快页面展示速度。Suspense组件在组件加载期间显示备选内容,确保了平滑的加载过渡。
171 0
|
4月前
|
前端开发 API
[译] 实战 React 18 中的 Suspense
[译] 实战 React 18 中的 Suspense
|
7月前
|
前端开发 数据可视化 UED
React的代码分割:使用React.lazy和Suspense进行性能优化
【4月更文挑战第25天】使用React的`React.lazy`和`Suspense`进行代码分割可优化性能,按需加载组件以提升应用启动速度和用户体验。`React.lazy`接收返回Promise的组件动态导入,而`Suspense`提供加载指示器,保证加载过程中的用户体验。适用于大型组件或路由应用,但需注意服务器配置、避免过度拆分和确保关键代码即时加载。合理运用能显著改善应用性能。
|
前端开发
React 16.x折腾记 - (7) 基于React+Antd封装聊天记录(用到React的memo,lazy, Suspense这些)
在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处 一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。
316 0
|
前端开发 JavaScript API
React 16.x 路线图公布,包括服务器渲染的 Suspense 组件及Hooks等
你可能已经在之前的博文和演讲中听说过“Hooks”,“Suspense” 和 “Concurrent Rendering”等功能。 在这篇文章中,我们将看看如何组合使用它们,并给出它们在 React 稳定版中的预计可用时间表。
686 0
|
7月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
382 0