React18 带来了什么

经历了v17的平缓过渡,React 3月29日正式发布了React v18版本。这个版本带来了一些十分重要的能力。但大家伙不必担心学不动,这个版本无破坏性更新,hooks 还在。以下是核心功能更新。

  1. 以 Concurrency(并发性) 为核心的底层渲染模型增强
  2. 新的 Suspense 带来的异步数据处理机制

作为以上能力的外显,补充了一些新的 API 和hooks.同时,将一些旧的 API 标记为 Deprecated 或者 Limited.

为了保证新版本的渐进升级,React 谨慎在释放新的能力,为了支持新的并发渲染特性,React 引入了一个新的 Root API:

1.png

使用了 createRoot 之后,会默认在后台使用并发渲染,以提升性能。当然,如果我们继续使用旧的 Render API,React 会按v17的方式去工作。以下是所有特性的一览表:

2.png

为了更好地理解 React 18,我强烈建议你阅读官方给出的以下两篇 blog。

同时,如果你还有一些疑惑,在 React 仓库的discussion 区,有一次很有趣的讨论:如何我是五岁小孩,你会如何给我解释 v18 带来的新特性:

接下来,我会对这个版本的核心能力一一解读。

https://github.com/reactwg/react-18/discussions/4

Concurrency 到底做了什么

Concurrent Render,作为这个版本引入的核心能力,到底做了什么事呢?事实上,在 v17 版本,React 就提出了一个实验性的模式:Concurrent Mode,它就是 Concurrent Render 的前身。首先需要明确的是,它并不是一个 feature,或者 API,而是类似于 v16 提出的 Fiber Reconciler,是对React 底层渲染模型的增强。

Concurrency 建立在 Fiber 的基础上。前情提要,我们来回顾一下 Fiber 做了什么。在 Fiber 之前,React 底层使用 stack reconciler 来更新 vDOM,这样的问题显而易见,任意一次 state 的变更,都会触发整颗 vDOM 树的更新,这是一个漫长的过程。Fiber 使用了 while-loop 的方式,来替代更新 vDOM 的更新过程,使用 while 循环,允许有一个寻找更新节点的钩子,来决定需要更新的部分,这也就是我们所说的分片能力,我们不必再等整个 vDOM 都更新。

但是,Fiber reconciler 的问题是:

  • 更新一旦启动,就无法暂停
  • 所有的更新一视同仁,无轻重缓急之分

为了解决以上的痛点,Concurrency 在v18版本作为核心能力出场了,在 Concurrency 的模式下,首先对更新的行为做了升级:

  • 渲染可以中断
  • 准备了多版本的UI来根据优先级渲染
  • 更新有优先级划分,可以划分为以下两类:
    1. Urgent updates:需要快速反馈的交互,如:键盘输入、点击、触摸等,在18之前,所有的更新都是这一类
    2. Transition updates:UI从一个视图到另一个视图的转换

以上的三种特性,正是 Concurrent Render 主要做文章的能力。那作为并发特性的外显,React 18 提供了以下 feature:

Automatic Batching Update:

可以称作批量更新,React 将多个状态更新,聚合到一次 render 中执行,以提升性能。在之前版本中,原生事件和 setTimeout 等行为中的多次更新都不会被合并。也就是说,每次 state 的变化,都会触发 re-render. 在新的版本中,如果你使用了新的 root API,以上的场景都会自动启动批量更新的能力。

before 18:

// Before React 18 only React events were batched
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will render twice, once for each state update (no batching)
}, 1000);

after 18:

// native event handlers or any other event are batched.
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);

当然,如果你不想应用这份能力,可以使用 flushSync 选择停用。

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

useTransition

我们在上文提到,React 18 将 UI 的更新分为 Urgent Update 以及 Transition Update,前者的优先级更高,在之前版本,所有的更新都被当做前者处理。所以,为了平滑升级,新版本提供了一个新的 API,useTransiton 来允许开发者手动指定哪些更新是 Transition 的部分。例如一个搜索按钮之后的视图变化,我们可以认为属于过渡视图,用户的预期中也是允许等待的,那我们就可以使用新的 API 来指定这些更新,让他们为更高的优先级的更新任务让步。

Suspense 下的全新异步数据获取机制

作为本次新版本的另外一个重量级特性,Suspense 在未来的开发中很值得我们期待。

Suspense 是一种异步数据获取的机制,对 Concurrent Render 的支持以及引入服务端。

rfcs/0213-suspense-in-react-18.md at main · reactjs/rfcs

它的原理是将子组件的渲染优先级降低,如果一个 Promise 还没有被 resolve,就会渲染 fallback。一旦 promise resolve,就触发渲染子组件的渲染。

这种异步数据的处理方式有很多优点:

  1. 数据获取和数据消费分离,例如以下的写法:
    // no suspense const isLoading, data = useData(id) if (isLoading) { return <Spinner /> } return data.map((item) => <>{item}</>) // suspense const data = useData(id) return ( <Suspense fallback={<Spinner />}> {data.map((item) => <>{item}</>)} </Suspense> }
  2. 声明式加载状态控制,可以更明确地明确哪些组件是一起加载的<Header /> <Suspense fallback={<Spinner />}> <Content /> <Footer /> </Suspense>
  3. 避免 race condition
    Suspense for Data Fetching (Experimental) - React

新的 Streaming SSR 模式

Concurrency 和 Suspense 另一个大展身手的场景是 SSR.这个版本对 SSR 的处理模式做了大大的增强。

New Suspense SSR Architecture in React 18 · Discussion #37 · reactwg/react-18

在历史版本中,SSR 是针对整个 APP 的加载:

  1. server:为整个 app 获取数据
  2. server:将整个 app 渲染为 HTML 并在 response 中返回给 client
  3. client:加载整个 app 的 JS 代码
  4. client:将 JS 逻辑关联到服务端产生的静态 HTML(hydration)
    Hydration: The process of rendering your components and attaching event handlers is known as “hydration”. It’s like watering the “dry” HTML with the “water” of interactivity and event handlers. (Or at least, that’s how I explain this term to myself.)

可以看出,在历史版本中,每一步必须完成针对整个 APP 的操作才能进入下一步。这样我们花费了大量的等待时间。

18版本优化的方式概而言之即是:**拆分。**具体的策略可以分为以下两个方面:

  1. Streaming HTML(server):流式返回 HTML。不再等待整个 app 的结构全部加载完之后才返回 client,而是根据优先级分批次返回。每次返回的内容包含 HTML 结构和一个<script> 标签,这里的script 用于页面结构的内联:<div hidden id="comments"> <!-- Comments --> <p>First comment</p> <p>Second comment</p> </div> <script> // This implementation is slightly simplified document.getElementById('sections-spinner').replaceChildren( document.getElementById('comments') ); </script>
  2. Selective Hydration(client),使用新的Suspense实现
1. 允许尽早进行 hydration 操作,即便剩余的HTML和JS还没有被加载。一个页面可能包含很多模块,某模块还没有被返回,页面中可以渲染 Suspense 提供的 fallback,已经加载过来的模块可以及时被 hydrate.
2. 允许根据用户交互来改变 hydration 的优先级(Selective Hydration)。这里的意思是,我们的 hydrate 操作可以被中断,举个例子,如果一个按钮的结构已经被返回,但还没有被 hydrate,它在等待另外一个模块 hydrate 完成。但此时,如果用户点击了一下按钮,React 会把按钮的优先级提高,暂停另一个模块的 hydrate,优先对按钮模块进行 hydrate,以便于快速地响应用户的交互诉求。之后再接着之前没有完成的工作。

整体来说,新的 SSR 支持了组件级别的流式渲染,在 server 端进行了提早的返回,在 client 端尽早地进行 hydrate,哪怕只返回了部分页面结构。并且对用户请求交互的部分优先进行 hydrate.

新的 SSR 模式下的API 变化

  • renderToNodeStream Deprecated → 使用 renderToPipeableStream 代替
  • 其他(deno等):renderToReadableStream
  • renderToStringrenderToStaticMarkup 被标记为 Limited
  • renderToStaticNodeStream render email

提供给第三方库的新 API


  1. useId: 在客户端和服务端同时生成新的unique id,以避免不必要的更新
  2. [useSyncExternalStore](<https://github.com/reactwg/react-18/discussions/70>):允许外部状态管理器,强制立即同步更新,以支持并发读取。这个新的 API 推荐用于所有 React 外部状态管理库
  3. useInsertionEffect:解决 CSS-in-JS 库在渲染中动态注入样式的性能问题。除非你已经构建了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 执行时机在 DOM 生成之后,Layout Effect 执行之前
  4. startTransition:用于过度视图的转换,为了兼容,手动触发
  5. useDeferredValue

新的 API 可以去官方文档深入了解。

严格模式的更新


React 未来会增加保留组件之前状态的能力,例如返回 Tab 页时保留之前的 Tab 浏览状态。为了检测是否是符合要求的组件写法,在18版本的严格模式的开发环境下,会模拟一个组件卸载再用保存的状态re-render的过程:

在以前,React 加载组件的逻辑为:

- `React mounts the component.`
    
    `* Layout effects are created.` 
    
    `* Effect effects are created.`

在 React 18 严格模式的开发环境,React 会模拟卸载并重载组件:

`* React mounts the component.`
    `* Layout effects are created.`
    `* Effect effects are created.`
`* React simulates unmounting the component.`
    `* Layout effects are destroyed.`
    `* Effects are destroyed.`
`* React simulates mounting the component with the previous state.`
    `* Layout effect setup code runs`
    `* Effect setup code runs`

不再支持IE


18 使用的一些现代浏览器能力,例如 microtasks 等能力不能在IE中很好polyfilled.

Other


  1. remove the "setState on unmounted component" warning
    初衷是防止内存泄漏,但在实际场景中,常见的是异步任务执行之后的setState,例如promise:
1. 如果promise执行得很快,它的结果由garbage-collected处理
2. 如果promise执行得很慢,它的结果并不会被生效
  1. Components can now render undefined
  2. Deprecated renderSubtreeIntoContainer
本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
React18 带来了什么
经历了v17的平缓过渡,React 3月29日正式发布了React v18版本。这个版本带来了一些十分重要的能力。但大家伙不必担心学不动,这个版本无破坏性更新,...
<<上一篇
下一篇>>