React 18 新特性概览 【React 面试必问】

大家好,我是前端小菜鸡。去年 React 17 发布的时候,一度被大家吐槽“无新特性”,因为没有添加任何面向开发人员的新功能,而主要侧重于升级简化 React 本身。那么即将到来的 React 18 又有哪些新特性呢?

React 官方团队在近日成立了 React 18 工作组,并在6月9日发布了 React 18 发布计划:

  • React 18 Alpha 版本 (现在可用)
  • React 18 Beta 版本 (几个月后)
  • React 18 RC 版本 (几个月后)
  • React 18 正式版本 (RC 版本发布的 2-4 周)

目前 alpha 版本已经可以使用,可以从 npm 上拉下来进行查看,只不过需要带上 @alpha 标签:

npm install react@alpha react-dom@alpha

React 18 工作组在 alpha 阶段只邀请了社区专家、库作者、开发团队成员参与试用并提供反馈,后期 beta 版本发布会邀请更多人参与讨论。点击链接 https://github.com/reactwg/react-18/discussions可以查看讨论内容。

下面我们一起来读一篇文章,了解 React 18 中即将到来的新特性,如果想看原文的话,我把链接放在这里:

原文链接:https://yagmurcetintas.com/journal/whats-new-in-react-18

了解新特性之前需要知道的几个概念

在讨论 React 18 新特性之前,我们先来了解几个概念:Server Side RenderingSuspenseHydration 。如果你对这些已经非常熟悉,那么就可以跳过这块内容。

Server Side Rendering

在常规的前端项目中,当浏览器请求页面,服务器会响应一系列的文件,其中包含一个几乎空的 HTML 文档和一个包括了整个应用内容的 bundle.js 。前端应用程序根据用户请求的路由,在 HTML 文档根标签下渲染对应的内容,这被称为客户端渲染 (Client-Side Rendering, CSR) 。在客户端渲染的过程中,浏览器会先渲染空的 HTML 文档,然后解析 bundle.js ,重新渲染应用程序以显示内容。用户首先会看到一个空白页,页面初始加载时间 (即首屏时间) 取决于客户端的网速以及需要下载和解析的 bundle.js 文件的大小。

译者注:随着前端应用的复杂度不断提升,如果还将整个应用打包为一份 bundle.js 体积将会非常巨大,无论是加载和解析都非常耗时。实际上现在大多前端项目都采用了按需加载的策略,即将 bundle 拆分为多个 chunk,服务器端根据加载当前页面需要的内容返回对应的 chunk ,这样可以极大提升首屏加载速度。

服务端渲染 (Server-Side Rendering, SSR) 允许在服务器端动态生成 HTML 文档。当浏览器请求页面,服务器端会先获取页面需要的数据,然后渲染出一份初始的 HTML 文档,这份 HTML 文档和包含整个应用逻辑的 bundle.js 文件一起作为响应内容发送给客户端。此时浏览器渲染的是已经有部分内容的 HTML 文档,用户不会看到空白页。应用逻辑则会在 bundle.js 解析完成后添加到应用程序中。

Hydration

如果你的应用程序包含大量内容,或者你预期用户处于弱网环境,那么使用 SSR 是有意义的,因为它可以提供更好的用户体验。当 bundle.js 在加载的过程中,用户已经可以浏览内容,即便整个应用还未正常运行 (此时页面是无法交互的)。当应用逻辑加载完毕,渲染 React 组件,事件处理函数被绑定到经过服务端渲染的 HTML 节点上,这个过程被称为 hydration(中文译为滋润)。把它想象成压缩的可膨胀毛巾或泥炭球。这两种产品在水滋润之前也没有任何作用。

Suspense

尽管看起来像是一个巧妙的魔术,但在使用 SSR 时,可能会遇到一些瓶颈。例如,服务器端动态渲染 HTML 页面,需要先获取所有的应用数据。这就意味着如果从另一台较慢的机器获取数据,则必须等待该过程完成。同时,在开始 hydration 之前,必须加载完所有的应用逻辑。此外,在用户可以开始交互之前,所有(页面元素)都必须完成 hydration。由于这样非常低效,React 团队在 2018 年引入了 Suspense 组件,在懒加载组件中允许组件等待异步操作的结果的时候展示一个 fallback UI。由于它正在改变 React 18 中的行为,我们将在下一节内容中进行讨论。

React 18 中的变化

新 Root API vs 旧 Root API

React app 是通过将根元素(最顶层组件)挂载到 DOM 上进行创建的。如果你使用脚手架创建了一个项目,你可以在 index.js 文件中找到 index.htmlApp.jsx 的关联,看起来是这样的:

import * as ReactDOM from "react-dom"
import App from "App"
// <App/> 组件直接挂载到 id 为 'app' 的 DOM 元素上:
ReactDOM.render(<App tab="home" />, document.getElementById("app"))

新的 Root API 使用 ReactDOM.createRoot() 创建一个新的根元素,然后 React app 在内部渲染:

import * as ReactDOM from "react-dom"
import App from "App"
// 使用 ReactDOM.createRoot() 创建一个根元素:
const root = ReactDOM.createRoot(document.getElementById("app"))
// 将 <App/> 组件渲染到根元素下:
root.render(<App tab="home" />)
// 如果 <App/> 更新,无需再重复传递初始 DOM 节点:
root.render(<App tab="profile" />)

但为什么重要呢?它的大部分好处都隐藏在幕后。为了能够使用下面提到的开箱即用的改进,需要在项目中将旧的 Root API 切换为新的 Root API

开箱即用的改进

一旦升级到 React18 并开始使用新的 Root API,这些改进就会被动地(在幕后)发生。正如我们上面提到的,如果您继续使用旧的 API,您将无法获得这些好处,因此请检查您的 index.js 文件并在需要时更新它。但即使您不这样做,您的应用程序也不会中断,所以一切都很好。

⭐️ Automatic batching (自动批处理)

批处理是 React 在幕后所做的事情,许多开发人员都没有意识到这一点。当您注意您的开发者控制台时,您会意识到,如果您在同一个事件处理函数中运行一系列的状态更新,即使您有两个状态更新,React 也只会重新渲染 UI 一次。这意味着 React 将它们批量(或分组)在一起,因此与其不断重复状态更新并重新渲染 UI 的步骤,它会同时执行状态更新并只渲染 UI 一次。

批处理是一种很好的机制,可以保护我们免受不必要的 UI 重新渲染,但 React 17 仅在事件处理函数中实现。而 Promise 链、异步代码或者原生事件处理函数的使用会打破这种行为(在这些场景中批处理会失效)。在 React 18 中,自动批处理会在原生事件处理函数、Promise 链和异步代码自动完成。

function handleClick() {
  // React 17: 重新渲染发生在所有状态都更新之后。这被称为批处理。
  // 这也是 React 18 的默认行为。
  setIsBirthday(b => !b)
  setAge(a => a + 1)
}
// 在下面代码块中,React 18 会自动批处理,但是 React 17 不会。
// 1. Promises:
function handleClick() {
  fetchSomething().then(() => {
    setIsBirthday(b => !b)
    setAge(a => a + 1)
  })
}
// 2. 异步代码:
setInterval(() => {
  setIsBirthday(b => !b)
  setAge(a => a + 1)
}, 5000)
// 3. 原生事件监听器:
element.addEventListener("click", () => {
  setIsBirthday(b => !b)
  setAge(a => a + 1)
})

如果需要,可以使用 ReactDOM.flushSync() 退出自动批处理,但是 React 团队不建议频繁使用它。

点击这里:https://github.com/reactwg/react-18/discussions/21可以查看 React 团队对自动批处理的解释。

允许组件渲染 undefined

React 18 之前,如果函数组件/类组件的 render 函数返回 undefined 或者不返回任何东西,会抛出一个错误,警告开发者要么返回 JSX 元素要么就返回 null 。这主要是为了发现开发人员忘记返回他们在组件中创建的元素的错误。但是 React 团队认为这种警告机制属于 linters 而不是库本身。因此在 React 18 中,您可以安全地返回 undefined 使组件不渲染任何东西。

Suspense 支持 SSR

之前版本的 React 并不支持在服务器端使用 suspense 。新的 pipeToNodeWritable API 提供了完整的内置 spspense 支持和 HTML 流。

未捕获的 Suspense

React 17 中,如果一个组件尝试异步加载,例如一个组件通过 React.lazy 加载,它会在它上面找到最近的 Suspense 边界,并渲染 fallback UI,直到组件被加载完成。然而,如果它的上面没有 Suspense 边界,则会抛出一个错误。在 React 18 中,如果没有找到 Suspense 边界,整个应用程序都会变为异步加载 ,意味着没有任何东西会被渲染,直到异步组件加载完成。

null 或者 undefined 的 Suspense fallback

在之前的版本中,如果一个 Suspense 边界没有传递 fallback prop ,或者传递的 fallbacknullundefined ,整个 Suspense 会被忽略,并使用下一个 Suspense 边界。如果没有更多边界,则会抛出一个错误。在 React 18 中,即使 fallback 传递 nullundefined ,也会使用这个 Suspense 边界,并且不渲染任何东西,直到异步组件加载完成。

并发特性

在编程领域中,并发是同时执行多个任务的能力。由于 React 在单线程上运行,它必须决定按什么顺序做什么。为此,React 使用了一个 dispatcher,它是一个回调注册表(就像 Node.js 中的调用栈)。在之前的 React 版本中,这个 API 对开发者来说是完全遥不可及的。React 18 添加了一些可选的并发功能,可以更智能地渲染内容并暴露此 API 的某些部分。这些新增的并发特性支持协同多任务、基于优先级的渲染、调度和允许中断,因此它们大大提高了用户体验。

React 还在 16.3 版本中引入了 StrictMode ,默认情况下是禁用的。它的创建是为了警告开发人员有关并发不兼容的代码,如果使用并发功能,这些代码可能会给应用程序引入 bug 。但是显然,在整个应用程序中启用 StrictMode 会将开发人员淹没在警告的海洋中,因此 React 团队决定构建并发功能而不是并发模式,并允许在使用并发特性的较小的部分启用 StrictMode,而不是为整个应用程序启用它。

因此,尽管 createRoot API 将整个应用程序更改为他们所谓的并发模式(concurrent mode),但组件的呈现还是一如既往,除非您使用我们在下面解释的并发功能。如果您在组件中使用并发功能,则该组件及其整个子树将同时呈现并 StrictMode 为其启用。

⭐️ startTransition

以前,React 有一个非常重要的规则:任何东西都不能干扰渲染。一旦组件状态改变并触发了重新渲染,就无法阻止它,直到组件重新渲染,页面才能响应(交互事件)。对于新的更新,每个状态更新都被归类为以下两类之一:要么是紧急更新 (urgent update),要么是过渡更新(transition update,简称为 transition)。紧急更新是用户直觉上期望在心跳中响应的操作,例如鼠标单击或按键。过渡更新是一些延迟可以接受的并且在很多时候都可以预期的操作,例如搜索查询。startTransition API 用来将内部的 setState 调用标记为 transitions,这意味着他们可中断的。过渡更新也同步运行,但 UI 在运行时不会被阻塞。

import { startTransition } from "react"
// 展示用户输入内容的紧急更新:
setInputValue(input)
// 被 startTransition 标记的过渡更新:
startTransition(() => {
// 一个非紧急、可中断更新的搜索查询:
setSearchQuery(input)
})

上面的例子来自 reactwg GitHub 讨论页面。如果 setSearchQuery(input) 未标记为过渡更新,则每次输入更改后都会锁定 UI。现在它被标记为非紧急,用户可以搜索一些东西并改变意见,并在根据输入变化更新 UI 之前决定导航到另一个页面,而不必等待不感兴趣的 UI 更新。

您甚至可以使用 useTransition 钩子跟踪过渡更新的 pending 状态,并根据需要向用户显示正在加载的 UI :

import { useTransition } from "react"
const [isPending, startTransition] = useTransition()
// 例如,在 pending 状态时,您可以展示一个 spinner:
{
isPending ? <Spinner /> : null
}

看看这个:https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786关于并发和 startTransition 特性的惊人解释以及 startTransition:https://github.com/reactwg/react-18/discussions/65 的真实例子。

useDeferredValue

useDeferredValue 钩子可帮助您在指定时间段内推迟更新 UI 的某些部分,同时保持页面响应。你也可以给它一个可选的 timeoutReact 将尝试尽快更新延迟值。如果在给定的 timeout 期限内未能完成,它将强制更新,在此过程中阻塞 UI。换句话说,延迟值通过过渡更新而不是紧急更新来更新,从而使您的 UI 在此过程中保持响应。

import { useDeferredValue } from "react"
const deferredValue = useDeferredValue(value, {
timeoutMs: 5000,
})

SuspenseList

SuspenseList 允许您协调它所包装的子树的 Suspense 节点的内容的出现顺序,即使数据以不同的顺序到达。通常,如果您有多个同级 Suspense 边界,它们会尽可能 resolve 。但是,您可能希望以特定顺序加载组件,无论它们以何种顺序自行 resolve

import { Suspense, SuspenseList } from "react"
<SuspenseList revealOrder="forwards">
<Suspense fallback="Loading first item...">
<FirstItem />
</Suspense>
<Suspense fallback="Loading second item...">
<SecondItem />
</Suspense>
<Suspense fallback="Loading third item...">
<ThirdItem />
</Suspense>
</SuspenseList>

在上面的例子中,即使第三个项目先加载,它也会渲染 Loading third item... ,直到加载第一个项目。加载第一个项目时,将呈现第一个项目,以及第二个和第三个的 fallback 。只有当第二个项目被加载时,三个项目才能被渲染。

revealOrder prop 可以取值 forwardsbackwardstogetherforwardsbackwards 允许内部的 Suspense 边界按照向前和向后的顺序进行 resolve 。另一方面,together 会等待所有的边界 resolve 完毕之后再去渲染。

你也可以给 SuspenseList 传一个 tail prop 。tail prop 可以取值 collapsed 和 hidden 。默认情况下, SuspenseList 渲染所有 fallback 。但是,如果您不想渲染任何 fallback ,则可以使用 tail="hidden" ,如果您只想渲染最多一个 fallback ,则可以使用 tail="collapsed"。这样,您可以创建许多 fallback ,而不必担心加载区域会变得混乱。

带有选择性 hydration 的流式 HTML

正如我们之前在概念部分讨论过的,服务器端渲染遵循以下步骤:

  • (服务器) 获取所有应用程序数据
  • (服务器) 渲染 HTML
  • (客户端) 加载 HTML 和应用程序逻辑
  • (客户端) hydrate 一切

除非当前步骤已完成,否则无法开始下一步,并且在所有 4 个步骤都完成后,应用程序变为可交互的。这意味着我们的应用程序至少有 4 个瓶颈会减慢我们的初始加载速度。React 18 提供了两个主要功能来帮助我们解决潜在的瓶颈。

在获取所有数据之前流式传输 HTML

如果您将页面的某些部分包裹在 Suspense 组件中,它不会等待该部分准备就绪,而是在其他组件完成后继续前进,同时为尚未准备好并使用 Suspense 组件包裹的部分显示一个 spinner 。当该部分的数据准备好时,React 将额外的 HTML(和一个小脚本)流式传输到客户端,并将内容准确地呈现在它应该出现的位置。有了这个,你就不必等到每条数据都被获取了,所以可以通过使用这个特性来解决步骤 1 中可能出现的瓶颈。(要使用它,你的数据获取库也必须实现它。React 具有与 Suspense 集成的开箱即用的 Server Component。)

如果 bundle.js 文件很大,第 3 步(应用程序逻辑的加载)可能比您想象的要长。为了避免这里的瓶颈,您可以执行code-splittinglazy-loading。这意味着根据请求的路径将应用程序逻辑分几个部分加载。出于各种原因,这很好,它更快,因为文件更小,并且您不会传输不必要的部分,因为用户可能不会访问应用程序提供的所有页面。

在所有代码加载之前 hydration

React 18 中,通过将组件包装在 中 Suspense,您只需告诉客户端不要阻止应用程序的其他部分等待该组件加载。因此,即使缺少某些 HTML,该应用程序也会开始 hydration

在所有组件 hydrated 之前与组件交互

使用 React 18,包裹在 Suspense 其中的组件的 hydration 不会阻止与已经 hydrated 的组件的交互。如果 Suspense 已经加载了几个包裹在其中的组件的 HTML ,它会从组件树中找到的第一个开始进行 hydration 。但是,如果用户在另一个组件的 hydration 期间与其他一个交互(例如不耐烦地点击它),它将停止另一个组件的 hydration ,并优先考虑用户尝试与之交互的组件的 hydration 。它还记录事件并在组件 hydration 后再次调度它。这称为选择性 hydration 作用。

结语

我们只是想说我们多么欣赏 React 不断发展的环境。React 是一个了不起的工具,它每年都在变得更好。React 团队非常棒,感谢您所做的所有工作。你们真是不可思议。

上一篇:12道 javaScript 经典逻辑题,是否承载着你的回忆


下一篇:Vite2 + Vue3 + TypeScript + Pinia 搭建一套企业级的开发脚手架【值得收藏】