React 18 带来了什么-官方即将支持状态保持

1,914 阅读7分钟

(由于现在 React18 还没正式发布太多的文档,很多概念和内容是我从多个来源拼凑而来,里面包含了很多我个人的理解,可能到 React18 正式发布的时候,会有些许错误,写这个文章仅仅是满足一下猎奇心理。所以如果你是在未来的某个时间内看到这篇文章,你记得去阅读官网的内容,并以官网的内容为主。)

其实 React18@alpha 已经发布有一段时间了,因为我最近分到一个调研-- “UMI 如何支持 react@18 alpha”。(要不然,我应该会继续蹲。)

所以就开始看了看相关的文档和新闻。

比较好的消息是,你可以非常平滑的升级到 React18。

比如在 umi 中,抛开测试和兼容之类的代码,仅仅只需要修改一行代码就可以支持。

- import { hydrate, render } from 'react-dom';
+ import ReactDOM, { hydrate, render } from 'react-dom';
- if (window.g_useSSR) {
- hydrate(rootContainer, rootElement, callback);
- } else {
- render(rootContainer, rootElement, callback);
- }

+ reactRoot = (ReactDOM as any).createRoot(rootElement, {
+   hydrate: window.g_useSSR,
+ });

+ reactRoot.render(rootContainer);

并且改完从业务侧,页面代码中都无需做任何修改项目就可以正常的运行。 接着你就可以通过选择性的添加 React18 的新特性到你的某些新页面或者优化某些场景。

(看到这里不得不吐槽一下 React Native,发一个小版本都前后不兼容啊!)

开箱即用

当你简单的更改了类似上面的代码,你将直接享受到 React18 开箱即用的一些功能。

  • 自动批处理以减少渲染
  • Suspense 的 SSR 支持(全新的 SSR 架构)

自动批处理以减少渲染

批处理使 React 将多个状态更新分组到单个重新渲染中以获得更好的性能。这个特性在现在的 React17 中就已经有了,但是在不同的上下文中交互的时候,将不支持批处理。现在 React18 中,增加了对网络请求,promise、setTimeout等事件的自动批处理支持。

React17 - Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 18 and later DOES batch these:
      setCount(c => c + 1);
      setFlag(f => !f);
      // React will only re-render once at the end (that's batching!)
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

Suspense 的 SSR 支持

之前在服务端渲染上对 Suspense 的支持不是很好,现在你可以通过使用新的 API pipeToNodeWritable 来使用全新的 SSR 架构。

比如我们期望最终打开的页面如下:(绿色代表用户可交互)

react18-ssr0.png (图片来自React18 工作组 discussions 37)

当你没有使用 SSR 功能时,我们的页面都会在页面启动的时候,经历短暂的白屏阶段,这是由于此时浏览器发起了对 js 的请求和加载,页面之后等待这些 js 文件被下载完成之后,才会被执行,渲染出页面。

当使用 SSR 时,React 会在服务端将组件渲染成 Html 并发送给用户,此时用户将会看到页面的基本框架,只能进行一些内置 Web 交互(如链接和表单输入)。

react18-ssr1.png (此处,灰色说明屏幕的这些部分尚未完全交互)

之后浏览器照常加载 js 文件,当 js 文件加载完成之后,通过水合过程,将当前的 html 渲染成可交互的。

这个过程的专业名词是 “Hydrate”,在 vue 中被翻译成“激活”,React 社区更期望称之为“水合”或者“注水”来描述这个过程,就相当于将“水”注入到“干”的 Html 中。

这里也存在一个和不使用 SSR 一样的问题,当 js 被全部加载,页面组件被全部“水合”之前,页面研究是不可交互的。React18 就支持,让部分的页面优先加载完数据,优先执行水合。你的页面看起来大概是这样子的。

react18-ssr2.png

并发渲染(concurrent rendering)

当然你也可以选择性的使用 React18 的一些新功能,React18 加入了一个主要的可选机制,“并发渲染(concurrent rendering)”,这个是很多新功能的基础。 值得注意的是这里的“并发”使用的依旧是单线程。但是这个单线程可以被中断的。因此渲染可以在多个渲染任务之间交错进行,如用户交互,网络请求,计时器,动画和浏览器布局/绘制等等。 它的主要工作分配大致如下:

react18-cr.png

当渲染任务遭遇到更高级的渲染任务时将会被中断,然后优先执行优先级更高的任务,再任务完成之后,再回到原来的渲染任务上。

你可以通过一下一些新的 API 来告诉 React 哪些是优先级较高的任务。

  • startTransition
  • useDeferredValue
  • SuspenseList

startTransition 过渡更新

这是一个比较好理解的 API,通过使用 startTransition 来包裹一些 setState,声明他们是比较不重要的渲染行为。比如官方的例子里面提到的搜索场景。

当用户在搜索框中输入时,搜索框需要实时的显示用户的输入字符,然后通过网络请求(或者本地过滤数据),获取到新的列表数据,再更新列表。这里面会有两个 setState,一个是 input 的 value 绑定。一个是搜索之后的页面数据绑定。

import { startTransition } from 'react';


// 紧急:显示输入的内容
setInputValue(input);

// 将内部的任何状态更新标记为转换
startTransition(() => {
  // Transition: 显示结果
  setSearchQuery(input);
});

如果你需要在等待过渡渲染的时候执行一些表现,如 loading 操作之类的。 你可以使用 useTransition

import { useTransition } from 'react';


const [isPending , startTransition] = useTransition();
startTransition(() => {
  // Transition: 显示结果
  setSearchQuery(input);
});

{ isPending  &&  < Spinner /> }

useDeferredValue

推迟更新屏幕上不太重要的部分(这个还没有放出来文档)。

SuspenseList

协调加载指示器出现的顺序(这个还没有放出来文档)。

但是从 Suspense 的用法来看,预计与懒加载的优先级有关,可能是指定哪些加载优先执行。(我猜的,毕竟新的文档几乎都是这个概念。)

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

其他

除了上面提到的这些和协作多任务、基于优先级的渲染、调度和中断等有关的功能之外,还有值得关注的特性。

StrictMode 严格模式

你的组件将会被多次调用加载-卸载,以确保他们的状态正确。

React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components.

值得关注的是,这个特性是被默认开启的,其实现在的React中就已经有使用这个功能了。--- 快速刷新(Fast Refresh),开发时可以保持组件状态,同时编辑提供即时反馈。

Offscreen

新的 Offscreen API 允许 React 通过隐藏组件而不是卸载组件来保持这样的状态。为此,React 将调用与卸载时相同的生命周期钩子——但它也会保留 React 组件和 DOM 元素的状态。这就是 React 中的 keepalive 功能啊。

这是我所期待的一个能力,现在是使用 <div hidden={true} /> 实现。

当然它还可以用作预渲染页面,提前渲染用户即将到达的页面,有点类似 Next 中用 Link 链接的页面,会被提前渲染。

在优先级方面,Offscreen 是最低的,理论上它会被任何其他的渲染任务中断。

it will not be in the initial 18.0 release, but may come in an 18.x minor.

感谢阅读,有任何疑问可以通过评论一起讨论。喜欢这个文章的朋友,请给一个赞,喜欢我的朋友,可以关注一下我。感谢三连。

参考链接

zh-hans.reactjs.org/blog/2021/0… github.com/reactwg/rea… github.com/reactwg/rea… github.com/reactwg/rea… github.com/reactwg/rea… github.com/reactwg/rea… www.youtube.com/watch?v=bpV…