React V18.0.0

Concurrent Mode

Concurrent Mode(以下简称 CM)翻译叫并发模式,这个概念我已经听了好多年了,并且一度非常担忧

  • React 官方憋了好多年的大招,会不会是一个破坏性不兼容的超级大版本?就像 VUE v3 和 v2。
  • 现有的生态是不是都得跟着大版本升级?比如 ant design,ahooks 等。

随着对 CM 的了解,我发现它其实是人畜无害的。

CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI
在这里插入图片描述
在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。

在这里插入图片描述
在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。

CM 模式有点类似计算机的多任务处理,处理器在同时进行的应用程序之间快速切换,也许 React 应该改名叫 ReactOS 了。

这里举个例子:我们正在看电影,这时候门铃响了,我们要去开门拿快递。在 React 18 以前,一旦我们开始看电影,就不能被终止,必须等电影看完之后,才会去开门。而在 React 18 CM 模式之后,我们就可以暂停电影,等开门拿完快递之后,再重新继续看电影。

不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化

我们需要关注的是基于 CM 实现的上层功能,比如 Suspense、Transitions、streaming server rendering(流式服务端渲染), 等等。

React 18 的大部分功能都是基于 CM 架构实现出来的,并且这这是一个开始,未来会有更多基于 CM 实现的高级能力。

startTransition

我们如果要主动发挥 CM 的优势,那就离不开 startTransition。
React 的状态更新可以分为两类:

  • 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡,或者出问题了的感觉
  • 过渡更新(Transition updates):将 UI 从一个视图过渡到另一个视图。不需要即时响应,有些延迟是可以接受的。

我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。

这是因为 React 并不能自动识别哪些更新是优先级更高的。

const [inputValue, setInputValue] = useState();

const onChange = (e)=>{
    
    
  setInputValue(e.target.value);
  // 更新搜索列表
  setSearchQuery(e.target.value);
}

return (
  <input value={
    
    inputValue} onChange={
    
    onChange} />
)

比如以上示例,用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。

但是 React 确实没有能力自动识别。所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。

// 紧急的
setInputValue(e.target.value);
startTransition(() => {
    
    
  setSearchQuery(input); // 非紧急的
});

如上代码,我们通过 startTransition来标记一个非紧急更新,让该状态触发的变更变成低优先级的。

React 会在高优先级更新渲染完成之后,才会启动低优先级更新渲染,并且低优先级渲染随时可被其它高优先级更新中断。

当然,在低优先状态等待更新过程中,如果能有一个 Loading 状态,那就更好了。React 18 提供了 useTransition来跟踪 transition 状态。

自动批处理 Automatic Batching

批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如

function handleClick() {
    
    
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

setTimeout(() => {
    
    
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会 render 两次,每次 state 变化更新一次
}, 1000);

而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。

function handleClick() {
    
    
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
    
    
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}, 1000);

如果你在某种场景下不想使用批处理,你可以通过 flushSync来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)

import {
    
     flushSync } from 'react-dom';

function handleClick() {
    
    
  flushSync(() => {
    
    
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    
    
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

React 18 的批处理在绝大部分场景下是没有影响,但在 Class 组件中,如果你在两次 setState 中间读取了 state 值,会出现不兼容的情况,如下示例。

handleClick = () => {
    
    
  setTimeout(() => {
    
    
    this.setState(({
     
      count }) => ({
    
     count: count + 1 }));

    // 在 React17 及之前,打印出来是 { count: 1, flag: false }
    // 在 React18,打印出来是 { count: 0, flag: false }
    console.log(this.state);

    this.setState(({
     
      flag }) => ({
    
     flag: !flag }));
  });
};

当然你可以通过 flushSync来修正它。

handleClick = () => {
    
    
  setTimeout(() => {
    
    
    ReactDOM.flushSync(() => {
    
    
      this.setState(({
     
      count }) => ({
    
     count: count + 1 }));
    });

    // 在 React18,打印出来是 { count: 1, flag: false }
    console.log(this.state);

    this.setState(({
     
      flag }) => ({
    
     flag: !flag }));
  });
};

流式 SSR

SSR 一次页面渲染的流程大概为:

  1. 服务器 fetch 页面所需数据
  2. 数据准备好之后,将组件渲染成 string 形式作为 response 返回
  3. 客户端加载资源
  4. 客户端合成(hydrate)最终的页面内容

在传统的 SSR 模式中,上述流程是串行执行的,如果其中有一步比较慢,都会影响整体的渲染速度。

而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。

假设我们有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。

但如果 Comments 数据请求很慢,会拖慢整个流程。

在 React 18 中,我们通过 Suspense包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。

我们通过 Suspense包裹了 Comments 组件,那服务器首次返回的 HTML 是下面这样的,组件处通过 loading进行了占位。

当 组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。

猜你喜欢

转载自blog.csdn.net/wangxinxin1992816/article/details/124339806