React Suspense解决竞态条件详解

目录
  • 前言
  • Suspense
  • 执行机制
  • 实际应用
  • 好处:请求前置
  • 好处:解决竞态条件
  • 错误处理
  • 源码

前言

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

Suspense

React 16.6 新增了 <Suspense> 组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面(像是个 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 内容。

这听起来好像没什么,但是想想如果我们是以前会怎么做,我们可能会用一个 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 就只是一个常量而已,具体的处理逻辑会在以后的文章中慢慢讲解。

React 系列

  • React 之 createElement 源码解读
  • React 之元素与组件的区别
  • React 之 Refs 的使用和 forwardRef 的源码解读
  • React 之 Context 的变迁与背后实现
  • React 之 Race Condition

以上就是React Suspense解决竞态条件详解的详细内容,更多关于React Suspense竞态条件的资料请关注我们其它相关文章!

(0)

相关推荐

  • React Suspense前后端IO异步操作处理

    目录 简单介绍Suspense Suspense主要用法和场景 一. React18之前的做法 二. React18之后 Suspense配合前端表格组件处理前后端IO异步操作 简单介绍Suspense Suspense主要用来解决网络IO问题,它早在2018年的React 16.6.0版本中就已发布.它的相关用法有些已经比较成熟,有的相对不太稳定,甚至经历了重命名.删除: 在render函数中,我们可以写入一个异步请求,请求数据 react会从我们缓存中读取这个缓存 如果有缓存了,直接进行正常

  • react中Suspense的使用详解

    关于Suspense的使用,先来看下示例代码 const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div&

  • React18 中的 Suspense API使用实例详解

    目录 什么是新的 ReactJS Suspense API,什么时候应该使用它? 什么是Suspense API? 什么是 transition API? 最后 什么是新的 ReactJS Suspense API,什么时候应该使用它? 何时使用:当组件开始变大并且您在同一页面上有许多组件时,您可能希望开始优化下载到客户端浏览器的方式和时间. 为此,React 为您提供了lazyAPI,它允许您将组件标记为lazy,这意味着被lazy包裹的组件,将会在第一次真正使用时被加载,而不是页面初始化的时

  • React中Suspense及lazy()懒加载及代码分割原理和使用方式

    目录 React.lazy() 概括 为什么需要懒加载 如何进行代码分割 Suspense Suspense应用场景 Suspense实现原理 总结 Suspense和lazy()都是react中比较新的特性,在项目中使用还比较少,但是学习一下有助于在后面的项目中使用,同样可以一窥React未来的发展方向 React.lazy() 概括 顾名思义lazy()方法是用来对项目代码进行分割,懒加载用的.只有当组件被加载,内部的资源才会导入 为什么需要懒加载 在React的项目中import导入其他组

  • react  Suspense工作原理解析

    目录 Suspense 基本应用 Suspense 原理 基本流程 源码解读 - primary 组件 源码解读 - 异常捕获 源码解读 - 添加 promise 回调 源码解读-Suspense 首次渲染 primary 组件加载完成前的渲染 primary 组件加载完成时的渲染 利用 Suspense 自己实现数据加载 Suspense 基本应用 Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成.其中

  • React Suspense解决竞态条件详解

    目录 前言 Suspense 执行机制 实际应用 好处:请求前置 好处:解决竞态条件 错误处理 源码 前言 在上一篇<React 之 Race Condition>中,我们最后引入了 Suspense 来解决竞态条件问题,本篇我们来详细讲解一下 Suspense. Suspense React 16.6 新增了 <Suspense> 组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面(像是个 spinner),让它在用户等待的时候显示. 目前,Suspense 仅支

  • React Fiber 树思想解决业务实际场景详解

    目录 熟悉 Fiber 树结构 业务场景 熟悉 Fiber 树结构 我们知道,React 从 V16 版本开始采用 Fiber 树架构来实现渲染和更新机制. Fiber 在 React 源码中可以看作是一个任务执行单元,每个 React Element 都会有一个与之对应的 Fiber 节点. Fiber 节点的核心数据结构如下: type Fiber = { type: any, //类型 return: Fiber, //父节点 child: Fiber, // 指向第一个子节点 sibli

  • react编写可编辑标题示例详解

    目录 需求 初始需求 方案设计 方案一 span + contentEditable 思路 代码如下 在这个方案中遇到的问题 存在的问题 方案二 直接用input处理展示和编辑 踩到的坑 需求 因为自己换工作到了新公司,上周入职,以前没有使用过react框架,虽然前面有学习过react,但是并没有实践经验 这个需求最终的效果是和石墨标题修改实现一样的效果 初始需求 文案支持可编辑 用户点击位置即光标定位处 超过50字读的时候,超出部分进行截断 当用户把所有内容删除时,失去焦点时文案设置为 “无文

  • 在JPA的@Query注解中使用limit条件(详解)

    在@Query注解注释的JPQL语句中写limit语句是会报错的 unexpected token :limit near line .... 解决方法是讲@Query注解中的limit语句去掉,然后传一个Pageable pageable=new PageRequest(offset,limit)进去 示例代码: controller import java.util.List; import org.springframework.beans.factory.annotation.Autow

  • 利用CDN加速react webpack打包后的文件详解

    此文不介绍webpack基本配置,如果对基本配置有疑问请查阅官方文档. 1.配置webpack.config.js 将output.publicPath改成上传到的cdn地址, 例(对应上面上传配置): publicPath: "https://your_base_cdn_url" + process.env.NODE_ENV + "/cdn/" 打包 NODE_ENV=production node_modules/webpack/bin/webpack.js -

  • Java回溯法解决全排列问题流程详解

    题目描述: 给定一不重复的数组,返回其具有的所有全排列(使用 List<List > 返回) 思路: 以数组 nums = [1, 2, 3] 为例,其具有的解空间可以用这样一棵树表示,相比看到这里大家就可以知道,这是一道可以用 回溯法 解决的题. 难点:如何保证不选到已经使用过的数组元素 —— 使用 used[] 数组标记该元素是否被使用过 细节请看代码注释 // 用于存储结果的数组 List<List<Integer>> ans = new ArrayList<

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

  • React使用refs操作DOM方法详解

    在react框架 甚至说是三大框架中都是不太支持大家直接去操作dom的 因为也没什么必要 当然也会有特殊情况 例如视频播放 强制动画 第三方插件的一些渲染或初始化 官方也给了我们对应的解决办法 那就是refs 我们来简单写一个 我们先在constructor中定义一个虚拟dom的控制 参考代码如下 constructor(props){ super(props); this.divDaimin = React.createRef() this.state = { } } 这里 我们就通过Reac

  • React Native系列之Recyclerlistview使用详解

    目录 recyclerlistview的介绍与使用 1.安装 2.概述和功能 3. RecyclerListView的使用 1.dataProvider 2.LayoutProvider 3.rowRenderer 4.onEndReached 5.onEndReachedThreshold 6.extendedState 7.scrollViewProps RecyclerListView所有属性 recyclerlistview的介绍与使用 1.安装 npm install --save r

  • React高阶组件使用教程详解

    目录 高阶组件(HOC) 概述 使用HOC解决横切关注点问题 不用改变原始组件使用组合 约定-将不相关的 props 传递给被包裹的组件 约定-最大化可组合性 约定-包装显示名称以便轻松调试 使用高阶组件的注意事项 高阶组件(HOC) 概述 是React复用组件逻辑的一种高级技巧,是一种基于React组合特性而形成的设计模式 高阶组件是参数为组件,返回值为新组件的函数 简单理解: 高阶组件本身是 函数,传参数是组件,返回值也是组件: 高阶组件不用关心数据是如何渲染的,只用关心逻辑即可 被包装的组

随机推荐