跳到主要内容

React18

2025年01月04日
柏拉文
越努力,越幸运

前言


React 18 更新文档

一、并发模式


并发渲染本身并不是一个功能。它是一个新的底层机制,使得 React 能够同时准备多个版本的 UI。你可以把并发视为一种底层实现的细节——它解锁了许多新功能因而非常有价值。React 在底层实现上使用了非常复杂的技术,如优先队列和多级缓冲。但是你不会在任何公共 API 中感知到这些。

同步模式: 不可中断的同步更新、渲染。意味着一旦开始渲染就无法中断, 当项目变得庞大、组件数量繁多时,就容易遇到CPU的瓶颈。同时,GUI渲染线程与JS线程是互斥的。所以JS脚本执行和浏览器布局、绘制不能同时执行。如果在渲染期间用户发生交互操作, 需要等待渲染完成。

并发模式: 基于 Fiber 架构时间切片 将大的渲染任务拆分成多个小块, 将不可中断的同步更新变为可中断的异步更新, 利用浏览器的空闲时间逐步执行避免长时间阻塞主线程,从而保持 UI 的流畅性。

并发模式 下更新, 会发生组件状态与外部数据源不同步而导致渲染不一致, 进而渲染撕裂的问提。useSyncExternalStore 通过在 subscribegetSnapshot 中实现精细的控制,保证了组件中的状态与外部数据源保持同步。这种机制确保了即使在应用中存在高频率的状态更新或并发模式下也不会出现状态不一致的情况。在 并发模式 下, React 可能会中断、暂停或重启渲染过程, useSyncExternalStore 可以确保即使在这种情况下,通过 getSnapshot 获取的状态仍然是最新的,有效避免了因组件状态与外部数据源不同步产生的问题。

React 18 之后,通过使用 createRoot 启动的根组件默认启用了并发特性。在这种模式下,使用 startTransition 可以将某些状态更新标记为低优先级更新,从而让界面在进行这些更新的同时仍然保持对用户输入的响应;而 useDeferredValue 则可以延迟那些计算或渲染开销较大的组件,从而平滑过渡而不造成卡顿。

二、自动批处理


批处理是指,当 React 在一个单独的重渲染事件中批量处理多个状态更新以此实现优化性能。如果没有自动批处理的话,我们仅能够在 React 事件处理程序中批量更新。在 React 18 之前,默认情况下 promisesetTimeout、原生应用的事件处理程序以及任何其他事件中的更新都不会被批量处理;但现在,这些更新内容都会被自动批处理:

React.js 18.x 之前, React合成事件生命周期钩子(除 componentDidUpdate 除外) 将会批量处理更新。但是在 Promise 结果setTimeout 或者原生事件处理程序中, 更新将会以同步的方式处理。批量处理原理为: 通过全局变量 executionContext 控制 React 执行上下文,指示 React 开启同步或者异步更新。executionContext 一开始被初始化为 NoContext,因此 React 默认是同步更新的。在合成事件生命周期钩子(除 componentDidUpdate 除外), 中, 一开始这个变量会赋值为一个 BatchedContext, 在此期间, 如果多次同步调用更新函数, 会走批量更新逻辑, 构造更新队列, 将多个更新函数加入到更新队列, 等到当前环境执行完毕后, 将 executionContext 恢复为之前状态, 根据更新队列进行调度更新。如果异步调用更新函数, 此时 executionContext 已经恢复之前状态, 直接开始更新任务。

React.js 18.x 之后, 几乎所有更新, 包括 Promise.thensetTimeout 或者原生事件处理程序等, 都将自动批处理。批量处理逻辑为: 在同一环境中, 无论同步调用还是异步调用, 多次更新函数任务的优先级是相同的, 属于同一批任务。这一批任务都通过 scheduleUpdateOnFiber 标记相同的优先级之后, 开始通过 ensureRootIsScheduled 进行调度更新, 对比上次等待的更新和本次更新的优先级, 如果相等, 则会终止这个这任务的调度流程, 复用已有的调度任务。因此, 多次触发更新只有第一次会进入到调度中

三、过渡更新


过渡(transition)更新React 中一个新的概念,用于区分紧急和非紧急的更新。

  • 紧急更新 对应直接的交互,如输入,点击,按压等。

  • 过渡更新 将 UI 从一个视图过渡到另一个。

像输入,点击,按压等紧急更新,需要立刻响应以符合人们对物理对象行为的预期。否则,他们就会觉得“不对劲”。但是,过渡更新不太一样,因为用户对感知到屏幕上的每一个中间值这件事是没有预期的。

import { startTransition } from 'react';

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

// 将任何内部的状态更新都标记为过渡更新
startTransition(() => {
// 过渡更新: 展示结果
setSearchQuery(input);
});

因此, 被包裹在 startTransition 中的更新会被处理为过渡更新,如果有紧急更新出现,比如点击或者按键,则会中断过渡更新。如果一个过渡更新被用户中断(比如,快速输入多个字符),React 将会抛弃未完成的渲染结果,然后仅渲染最新的内容。并发渲染中将会加入过渡更新,允许更新被中断。如果更新内容被重新挂起,过渡机制也会告诉 React 在后台渲染过渡内容时继续展示当前内容

四、新的 Suspense 特性


Suspense 允许你声明式地为一部分还没有准备好被展示的组件树指定加载状态。Suspense 使得 UI 加载状态成为了 React 编程模型中最高级的声明式概念。我们基于此能够构建更高级的功能。几年前,我们推出了一个受限制版的 Suspense。但是唯一支持的场景就是用 React.lazy 拆分代码,而且在服务端渲染时完全没有作用。在 React 18 中,我们已经支持了服务端 Suspense,并且使用并发渲染特性扩展了其功能。

React 18 中的 Suspense 在与 Transition API 结合时效果最好。如果你在 Transition 期间挂起,React 不会让已显示的内容被后备方案取代。相反,React 会延迟渲染,直到有足够的数据,以防止出现加载状态错误。

五、新的客户端和服务端渲染 API


5.1 createRoot

createRootrender 或者 unmount 创建根节点的新方法。请用它替代 ReactDOM.render。如果没有它,React 18 中的新功能就无法生效。

createRoothydrateRoot 都能接受一个新的可选参数叫做 onRecoverableError,它能在 React 在渲染或者激活过程发生错误后又恢复时,做日志记录对你进行通知。默认情况下,React 会使用 reportError,如果在老旧版本浏览器中,则会使用 console.error

5.2 hydrateRoot

hydrateRoot 服务端渲染的应用的新方法。使用它来替代 ReactDOM.hydrate 与新的 React DOM 服务端 API 一起使用。如果没有它,React 18 中的新功能就无法生效。

createRoothydrateRoot 都能接受一个新的可选参数叫做 onRecoverableError,它能在 React 在渲染或者激活过程发生错误后又恢复时,做日志记录对你进行通知。默认情况下,React 会使用 reportError,如果在老旧版本浏览器中,则会使用 console.error

5.3 renderToPipeableStream

renderToPipeableStream 用于 Node 环境中的流式渲染。

5.4 renderToReadableStream

renderToReadableStream 对新式的非主流运行时环境,比如 DenoCloudflare workers

六、开发环境严格模式


当你使用严格模式时,React 会对每个组件进行两次渲染,以便你观察一些意想不到的结果, 目的在于帮助开发人员发现意外的副作用。重复渲染有助于暴露出依赖于副作用的不一致行为(例如,在组件的不同渲染阶段中读写到全局变量), 确保应用能够适应React未来可能引入的并发(Concurrent)模式。严格模式如下所示:

import React from 'react';
import App from './App.tsx';
import ReactDOM from 'react-dom/client';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

React.js 18 之前, 取消了其中一次渲染的控制台日志,以便让日志更容易阅读。

React.js 18 之后, 为了解决社区对这个问题的困惑,官方取消了这个限制, 所以会有两次渲染日志。

七、新的 Hook


7.1 useId

useId 是一个新的 Hook,用于生成在客户端和服务端两侧都独一无二的 id,避免激活后两侧内容不匹配。它主要用于需要唯一 id 的,具有集成 API 的组件库。这个更新不仅解决了一个在React 17及更低版本中的存在的问题,而且它会在 React 18 中发挥更重要的作用,因为新的流式服务端渲染响应 HTML 的方式将是无序的,需要独一无二的 id 作为索引。

注意: useId 不是 为了生成 列表中的 keykey 应该根据你的数据生成。

7.2 useTransition

useTransitionstartTransition 让你能够将一些状态更新标记为过渡更新。默认情况下,状态更新都被视为紧急更新。React 将允许紧急更新(例如,更新一个文本输入内容)打断过渡更新(例如,渲染一个搜索结果列表)。

7.3 useDeferredValue

useDeferredValue 允许推迟渲染树的非紧急更新。这和防抖操作非常相似,但是有一些改进。它没有固定的延迟时间,React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。

7.4 useInsertionEffect

useInsertionEffect 是一个新的 Hook ,允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。除非你已经建立了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 将在 DOM 变更发生后,在 layout Effect 获取新布局之前运行。这个功能不仅解决了一个在 React 17 及以下版本中已经存在的问题,而且在 React 18 中更加重要,因为 React 在并发渲染时会为浏览器让步,给它一个重新计算布局的机会。

7.5 useSyncExternalStore

useSyncExternalStore 是一个新的 Hook,允许使用第三方状态管理来支持并发模式,并且能通过对 store 进行强制更新实现数据同步。对第三方数据源的订阅能力的实现上,消除了对 useEffect 的依赖,推荐任何 React 相关的第三方状态管理库使用这个新特性。

八、其他变化


优化、修复了一些 Bug

一、提高内存利用率

一、允许组件渲染 undefined

三、修复上下文无法在挂起的树内传播

四、首次渲染时不要 patch 控制台

五、从个别事件(如点击),同步送出 useEffect 结果

六、修复 Safari 在追加 iframe 时忽略 setState 的问题