跳到主要内容

React18

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

一、React18


1.1 并发模式

React 18 引入新的根 API root = createRoot(); root.render(), 用于替换之前的版本的 ReactDOM.render() 方法。createRoot() 开启 React 并发模式。在并发模式下:

  1. 开启自动批处理

  2. 通过useTransitionuseDeferredValue 开启并发特性, 进而开启并发更新: React 并发更新利用 fiber 结构和时间切片的机制, 将一个大任务分解成多个小任务,然后按照任务的优先级和线程的占用情况,对任务进行调度。从同步不可中断更新变成了异步可中断更新。调度逻辑如下:

    1. 对于每个更新,为其分配一个优先级 lane,用于区分其紧急程度

    2. 通过 Fiber 结构将不紧急的更新拆分成多段更新,并通过宏任务的方式将其合理分配到浏览器的帧当中。这样就能使得紧急任务能够插入进来

    3. 高优先级的更新会打断低优先级的更新,等高优先级更新完成后,再开始低优先级更新

    4. 低优先级任务如果一直得不到执行, 下一轮会以同步的优先级执行, 防止低优先级任务饿死。

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

1.2 自动批处理

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 进行调度更新, 对比上次等待的更新和本次更新的优先级, 如果相等, 则会终止这个这任务的调度流程, 复用已有的调度任务。因此, 多次触发更新只有第一次会进入到调度中

1.3 引入新的 API

客户端渲染 createRoot: 用于客户端创建 React 渲染的根节点, 有 renderunmount 两个方法

水合服务端渲染 hydrateRoot: 主要作用是在客户端接管从服务器端渲染的应用程序,并将应用程序的状态与服务器端保持一致。

1.4 引入新的 Hooks

React 18 引入 useTransitionuseDeferredValueuseSyncExternalStore 等新 Hooks

  1. useTransition: 用于在不阻塞 UI 的情况下, 更新其他状态。

  2. useDeferredValue: 用于延迟更新 UI 的某些部分

  3. useSyncExternalStore: 用于订阅外部的 store

1.5 开发环境严格模式

当你使用严格模式时,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 之后, 为了解决社区对这个问题的困惑,官方取消了这个限制, 所以会有两次渲染日志。