跳至主要内容

新架构已至

·22 分钟阅读
The React Team
React 团队
@reactjs / @reactnative

默认启用新架构的 React Native 0.76 现在已在 npm 上可用!

0.76 版本发布博文中,我们分享了此版本中包含的重大更改列表。在这篇文章中,我们将概述新架构以及它如何塑造 React Native 的未来。

新架构增加了对现代 React 功能的全面支持,包括 SuspenseTransitions自动批处理useLayoutEffect。新架构还包括新的 原生模块原生组件系统,让您可以使用类型安全的代码直接访问原生接口,而无需桥接。

此版本是我们自 2018 年以来一直在进行的基础性 React Native 重写的结果,并且我们格外小心地使新架构成为大多数应用的渐进式迁移。在 2021 年,我们创建了 新架构工作组,与社区合作,确保整个 React 生态系统的平稳升级体验。

大多数应用都能够以与任何其他版本相同的努力程度采用 React Native 0.76。最流行的 React Native 库已经支持新架构。新架构还包括一个自动互操作层,以实现与以旧架构为目标的库的向后兼容性。

在过去几年的开发中,我们的团队公开分享了我们对新架构的愿景。如果您错过了任何这些演讲,请在此处查看:

什么是新架构

新架构是对 React Native 主要底层系统的完全重写,包括组件的渲染方式、JavaScript 抽象如何与原生抽象通信以及如何在不同线程之间调度工作。虽然大多数用户不必考虑这些系统如何工作,但这些更改带来了改进和新功能。

在旧架构中,React Native 使用异步桥接与原生平台通信。为了渲染组件或调用原生函数,React Native 需要序列化原生函数调用并将其排队到桥接中,这将异步处理。此架构的优点是主线程永远不会因渲染更新或处理原生模块函数调用而被阻塞,因为所有工作都在后台线程上完成。

但是,用户期望对交互的即时反馈,以感觉像原生应用。这意味着某些更新需要同步渲染以响应用户输入,可能会中断任何正在进行的渲染。由于旧架构仅是异步的,因此我们需要重写它以允许异步和同步更新。

此外,在旧架构中,通过桥接序列化函数调用很快成为瓶颈,尤其是对于频繁更新或大型对象。这使得应用难以可靠地实现 60+ FPS。还存在同步问题:当 JavaScript 和原生层失去同步时,无法同步协调它们,从而导致诸如列表显示空白帧和由于中间状态渲染而导致的视觉 UI 跳跃之类的错误。

最后,由于旧架构使用原生层级结构保留 UI 的单个副本,并就地修改该副本,因此布局只能在单个线程上计算。这使得不可能处理诸如用户输入之类的紧急更新,并且布局无法同步读取,例如在布局效果中读取以更新工具提示的位置。

所有这些问题意味着无法正确支持 React 的并发功能。为了解决这些问题,新架构包括四个主要部分:

  • 新的原生模块系统
  • 新的渲染器
  • 事件循环
  • 移除桥接

新的模块系统允许 React Native 渲染器同步访问原生层,这使其能够异步和同步地处理事件、调度更新和读取布局。新的原生模块也默认延迟加载,从而为应用带来显著的性能提升。

新的渲染器可以处理跨多个线程的多个进行中树,这允许 React 处理多个并发更新优先级,无论是在主线程还是后台线程上。它还支持同步或异步地从多个线程读取布局,以支持更灵敏的 UI,而不会出现卡顿。

新的事件循环可以按明确定义的顺序处理 JavaScript 线程上的任务。这允许 React 中断渲染以处理事件,以便紧急用户事件可以优先于较低优先级的 UI 过渡。事件循环也与 Web 规范对齐,因此我们可以支持浏览器功能,如微任务、MutationObserverIntersectionObserver

最后,移除桥接可以加快启动速度,并实现 JavaScript 和原生运行时之间的直接通信,从而最大限度地降低切换工作的成本。这也允许更好的错误报告、调试以及减少由未定义行为引起的崩溃。

新架构现在已准备好在生产环境中使用。它已经在 Meta 的 Facebook 应用和其他产品中大规模使用。我们在为我们的 Quest 设备开发的 Facebook 和 Instagram 应用中成功使用了 React Native 和新架构。

我们的合作伙伴已经使用新架构在生产环境中使用了几个月:请查看 ExpensifyKraken 的这些成功案例,并尝试 BlueSky 的新版本。

新的原生模块

新的原生模块系统是对 JavaScript 和原生平台如何通信的重大重写。它完全用 C++ 编写,这解锁了许多新功能:

  • 与原生运行时的同步访问
  • JavaScript 和原生代码之间的类型安全
  • 跨平台代码共享
  • 默认延迟模块加载

在新的原生模块系统中,JavaScript 和原生层现在可以通过 JavaScript 接口 (JSI) 彼此同步通信,而无需使用异步桥接。这意味着您的自定义原生模块现在可以同步调用函数、返回值并将该值传递回另一个原生模块函数。

在旧架构中,为了处理来自原生函数调用的响应,您需要提供回调,并且返回值需要是可序列化的。

// ❌ Sync callback from Native Module
nativeModule.getValue(value => {
// ❌ value cannot reference a native object
nativeModule.doSomething(value);
});

在新架构中,您可以同步调用原生函数。

// ✅ Sync response from Native Module
const value = nativeModule.getValue();

// ✅ value can be a reference to a native object
nativeModule.doSomething(value);

借助新架构,您最终可以利用 C++ 原生实现的全部功能,同时仍然可以从 JavaScript/TypeScript API 访问它。新的模块系统支持 用 C++ 编写的模块,因此您可以编写一次模块,它就可以在所有平台上工作,包括 Android、iOS、Windows 和 macOS。用 C++ 实现模块可以实现更细粒度的内存管理和性能优化。

此外,借助 Codegen,您的模块可以定义 JavaScript 层和原生层之间强类型的契约。根据我们的经验,跨边界类型错误是跨平台应用中最常见的崩溃来源之一。Codegen 让您克服这些问题的同时,还为您生成样板代码。

最后,模块现在是延迟加载的:它们仅在实际需要时才加载到内存中,而不是在启动时加载。这缩短了应用启动时间,并使其在应用程序复杂性增长时保持较低水平。

诸如 react-native-mmkv 之类的流行库已经看到了迁移到新的原生模块的好处。

“新的原生模块大大简化了 react-native-mmkv 的设置、自动链接和初始化。得益于新架构,react-native-mmkv 现在是一个纯 C++ 原生模块,这使其可以在任何平台上工作。新的 Codegen 允许 MMKV 完全类型安全,这通过强制执行空安全修复了长期存在的 NullPointerReference 问题,并且能够同步调用原生模块函数使我们能够用新的原生模块 API 替换自定义 JSI 访问。”

Marc Rousavyreact-native-mmkv 的创建者

新的渲染器

我们还完全重写了原生渲染器,增加了以下几个好处:

  • 更新可以在不同线程上以不同的优先级渲染。
  • 布局可以同步且跨不同线程读取。
  • 渲染器用 C++ 编写,并在所有平台上共享。

更新后的原生渲染器现在将视图层级结构存储在不可变的树结构中。这意味着 UI 以无法直接更改的方式存储,从而允许线程安全地处理更新。这使其能够处理多个进行中树,每个树代表用户界面的不同版本。因此,更新可以在后台渲染而不会阻塞 UI(例如在过渡期间),或者在主线程上渲染(以响应用户输入)。

通过支持多线程,React 可以中断低优先级更新以渲染紧急更新(例如由用户输入生成的更新),然后在需要时恢复低优先级更新。新的渲染器还可以同步且跨不同线程读取布局信息。这使得能够为低优先级更新进行后台计算,并在需要时进行同步读取,例如重新定位工具提示。

最后,用 C++ 重写渲染器使其可以在所有平台上共享。这确保了相同的代码在 iOS、Android、Windows、macOS 和任何其他 React Native 支持的平台上运行,提供一致的渲染功能,而无需为每个平台重新实现。

这是朝着我们的 多平台愿景迈出的重要一步。例如,视图扁平化是 Android 独有的优化,用于避免深层布局树。新的渲染器,具有共享的 C++ 核心,将此功能带到 iOS。这种优化是自动的,不需要设置,它随着共享渲染器免费提供。

通过这些更改,React Native 现在完全支持并发 React 功能,如 Suspense 和 Transitions,从而更容易构建对用户输入快速响应而不会出现卡顿、延迟或视觉跳跃的复杂用户界面。未来,我们将利用这些新功能为内置组件(如 FlatList 和 TextInput)带来更多改进。

诸如 Reanimated 之类的流行库已经在使用新的渲染器。

“Reanimated 4(目前正在开发中)引入了一个新的动画引擎,该引擎直接与新的渲染器协同工作,使其能够跨不同线程处理动画和管理布局。新的渲染器的设计才是真正使这些功能能够在不依赖大量变通方法的情况下构建的关键。此外,由于它是在 C++ 中实现的并在平台之间共享,因此 Reanimated 的大部分可以编写一次,从而减少特定于平台的问题,最大限度地减少代码库并简化树外平台的采用。”

Krzysztof MagieraReanimated 的创建者

事件循环

新架构使我们能够实现明确定义的事件循环处理模型,如本 RFC 中所述。此 RFC 遵循 HTML 标准中描述的规范,它描述了 React Native 应如何在 JavaScript 线程上执行任务。

实现明确定义的事件循环弥合了 React DOM 和 React Native 之间的差距:React Native 应用程序的行为现在更接近 React DOM 应用程序的行为,从而更容易一次学习,随处编写。

事件循环为 React Native 带来了许多好处:

  • 能够中断渲染以处理事件和任务
  • 更接近 Web 规范
  • 更多浏览器功能的基础

借助事件循环,React 能够可预测地对更新和事件进行排序。这允许 React 用紧急用户事件中断低优先级更新,并且新的渲染器允许我们独立渲染这些更新。

事件循环还将事件和任务(如计时器)的行为与 Web 规范对齐,这意味着 React Native 的工作方式更像用户在 Web 中熟悉的方式,并允许 React DOM 和 React Native 之间更好地进行代码共享。

它还允许实现更符合规范的浏览器功能,如微任务、MutationObserverIntersectionObserver。这些功能尚未准备好在 React Native 中使用,但我们正在努力在未来将它们带给您。

最后,事件循环和新的渲染器更改以支持同步读取布局,使 React Native 能够为 useLayoutEffect 添加适当的支持,以同步读取布局信息并在同一帧中更新 UI。这允许您在元素显示给用户之前正确地定位它们。

有关更多详细信息,请参阅 useLayoutEffect

移除桥接

在新架构中,我们还完全移除了 React Native 对桥接的依赖,用 JSI 实现 JavaScript 和原生代码之间的直接、高效通信来取代它。

移除桥接通过避免桥接初始化来缩短启动时间。例如,在旧架构中,为了向 JavaScript 提供全局方法,我们需要在启动时在 JavaScript 中初始化一个模块,从而导致应用启动时间略有延迟。

// ❌ Slow initialization
import {NativeTimingModule} from 'NativeTimingModule';
global.setTimeout = timer => {
NativeTimingModule.setTimeout(timer);
};

// App.js
setTimeout(() => {}, 100);

在新架构中,我们可以直接绑定来自 C++ 的方法。

// ✅ Initialize directly in C++
runtime.global().setProperty(runtime, "setTimeout", createTimer);
// App.js
setTimeout(() => {}, 100);

重写还改进了错误报告,特别是对于启动时的 JavaScript 崩溃,并减少了由未定义行为引起的崩溃。如果发生崩溃,新的 React Native DevTools 简化了调试并支持新架构。

桥接仍然保留用于向后兼容性,以支持向新架构的渐进式迁移。将来,我们将完全移除桥接代码。

渐进式迁移

我们预计大多数应用可以像升级到任何其他版本一样轻松地升级到 0.76。

当您升级到 0.76 时,默认情况下会启用新架构和 React 18。但是,要使用并发功能并获得新架构的全部好处,您的应用和库将需要逐步迁移以完全支持新架构。

当您首次升级时,您的应用将在新架构上运行,并具有与旧架构的自动互操作层。对于大多数应用,这将无需任何更改即可工作,但是互操作层存在 已知限制,因为它不支持访问自定义 Shadow Nodes 或并发功能。

要使用并发功能,应用还需要更新以支持 并发 React,方法是遵循 React 规则。要将您的 JavaScript 代码迁移到 React 18 及其语义,请遵循 React 18 升级指南

总体策略是在不破坏现有代码的情况下让您的应用程序在新架构上运行。然后,您可以按照自己的节奏逐步迁移您的应用。对于已将所有模块迁移到新架构的新表面,您可以立即开始使用并发功能。对于现有表面,您可能需要解决一些问题并在添加并发功能之前迁移模块。

我们与最流行的 React Native 库合作,以确保对新架构的支持。超过 850 个库已经兼容,包括所有每周下载量超过 20 万次的库(约占下载库的 10%)。您可以在 reactnative.directory 网站上检查库与新架构的兼容性。

有关升级的更多详细信息,请参阅下面的 如何升级

新功能

新架构包括对 React 18、并发功能和 React Native 中的 useLayoutEffect 的全面支持。有关 React 18 功能的完整列表,请参阅 React 18 博客文章

Transitions

Transitions 是 React 18 中的一个新概念,用于区分紧急更新和非紧急更新。

  • 紧急更新反映直接交互,如键入和按下。
  • 过渡更新将 UI 从一个视图过渡到另一个视图。

紧急更新需要立即响应,以符合我们对物理对象行为的直觉。但是,过渡是不同的,因为用户不期望看到屏幕上的每个中间值。在新架构中,React Native 能够支持分别渲染紧急更新和过渡更新。

通常,为了获得最佳用户体验,单个用户输入应同时产生紧急更新和非紧急更新。与 ReactDOM 类似,诸如 presschange 之类的事件被视为紧急事件并立即渲染。您可以在输入事件中使用 startTransition API 来告知 React 哪些更新是“过渡”,可以推迟到后台。

import {startTransition} from 'react';

// Urgent: Show the slider value
setCount(input);

// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setNumberOfTiles(input);
});

将紧急事件与过渡分离可以实现更灵敏的用户界面和更直观的用户体验。

这是没有过渡的旧架构和具有过渡的新架构的比较。想象一下,每个图块都不是带有背景颜色的简单视图,而是一个包含图像和其他组件的丰富组件,这些组件渲染起来很昂贵。使用 useTransition 后,您可以避免使用更新来拖垮您的应用并落后。

A video demonstrating an app rendering many views (tiles) according to a slider input. The views are rendered in batches as the slider is quickly adjusted from 0 to 1000.
之前:渲染图块,但不将其标记为过渡。
A video demonstrating an app rendering many views (tiles) according to a slider input. The views are rendered in batches as the slider is quickly adjusted from 0 to 1000. There are less batch renders in comparison to the next video.
之后:使用过渡渲染图块,以中断陈旧状态的进行中渲染。

有关更多信息,请参阅 对并发渲染器和功能的支持

自动批处理

升级到新架构后,您将受益于 React 18 的自动批处理。

自动批处理允许 React 在渲染时将更多状态更新批处理在一起,以避免渲染中间状态。这使 React Native 更快,并且不易受到滞后影响,而无需开发人员编写任何额外的代码。

A video demonstrating an app rendering many views according to a slider input. The slider value is adjusted from 0 to 1000 and the UI slowly catches up to rendering 1000 views.
之前:使用旧版渲染器渲染频繁的状态更新。
A video demonstrating an app rendering many views according to a slider input. The slider value is adjusted from 0 to 1000 and the UI resolves to 1000 views faster than the previous example, without as many intermediate states.
之后:使用自动批处理渲染频繁的状态更新。

在旧架构中,渲染了更多中间状态,即使滑块停止移动,UI 也会不断更新。新架构渲染的中间状态更少,并且由于自动批处理更新,渲染完成得更快。

有关更多信息,请参阅 对并发渲染器和功能的支持

useLayoutEffect

在新架构中,基于事件循环和同步读取布局的能力,我们为 React Native 添加了对 useLayoutEffect 的适当支持。

在旧架构中,您需要使用异步 onLayout 事件来读取视图的布局信息(这也是异步的)。因此,至少会有一帧布局不正确,直到读取和更新布局为止,从而导致诸如工具提示放置在错误位置之类的问题。

// ❌ async onLayout after commit
const onLayout = React.useCallback(event => {
// ❌ async callback to read layout
ref.current?.measureInWindow((x, y, width, height) => {
setPosition({x, y, width, height});
});
}, []);

// ...
<ViewWithTooltip
onLayout={onLayout}
ref={ref}
position={position}
/>;

新架构通过允许在 useLayoutEffect 中同步访问布局信息来解决此问题。

// ✅ sync layout effect during commit
useLayoutEffect(() => {
// ✅ sync call to read layout
const rect = ref.current?.getBoundingClientRect();
setPosition(rect);
}, []);

// ...
<ViewWithTooltip ref={ref} position={position} />;

此更改允许您同步读取布局信息并在同一帧中更新 UI,从而允许您在元素显示给用户之前正确地定位它们。

A view that is moving to the corners of the viewport and center with a tooltip rendered either above or below it. The tooltip is rendered after a short delay after the view moves
在旧架构中,布局在 onLayout 中异步读取,导致工具提示的位置延迟。
A view that is moving to the corners of the viewport and center with a tooltip rendered either above or below it. The view and tooltip move in unison.
在新架构中,可以在 useLayoutEffect 中同步读取布局,在显示之前更新工具提示位置。

有关更多信息,请参阅 同步布局和效果的文档。

全面支持 Suspense

Suspense 让您可以声明式地指定组件树的一部分的加载状态(如果它尚未准备好显示)。

<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>

我们几年前引入了有限版本的 Suspense,React 18 添加了全面支持。到目前为止,React Native 还无法支持 Suspense 的并发渲染。

新架构包括对 React 18 中引入的 Suspense 的全面支持。这意味着您现在可以在 React Native 中使用 Suspense 来处理组件的加载状态,并且悬挂内容将在后台渲染,同时显示加载状态,从而为可见内容上的用户输入提供更高的优先级。

有关更多信息,请参阅 React 18 中 Suspense 的 RFC

如何升级

要升级到 0.76,请按照 发布博文中的步骤操作。由于此版本也升级到 React 18,因此您还需要遵循 React 18 升级指南

由于与旧架构的互操作层,这些步骤对于大多数应用来说应该足以升级到新架构。但是,要充分利用新架构并开始使用并发功能,您需要迁移您的自定义原生模块和原生组件以支持新的原生模块和原生组件 API。

如果不迁移您的自定义原生模块,您将无法获得共享 C++、同步方法调用或来自 codegen 的类型安全的好处。如果不迁移您的原生组件,您将无法使用并发功能。我们建议尽快将所有原生组件和原生模块迁移到新架构。

注意

在未来的版本中,我们将移除互操作层,模块将需要支持新架构。

应用

如果您是应用开发者,要完全支持新架构,您将需要升级您的库、自定义原生组件和自定义原生模块以完全支持新架构。

我们与最流行的 React Native 库合作,以确保对新架构的支持。您可以在 reactnative.directory 网站上检查库与新架构的兼容性。

如果您的应用依赖的任何库尚不兼容,您可以:

  • 向库提交问题,并要求作者迁移到新架构。
  • 如果库未维护,请考虑具有相同功能的替代库。
  • 选择退出新架构,同时迁移这些库。

如果您的应用具有自定义原生模块或自定义原生组件,我们希望它们能够正常工作,这要归功于我们的 互操作层。但是,我们建议将它们升级到新的原生模块和原生组件 API,以完全支持新架构并采用并发功能。

请遵循以下指南将您的模块和组件迁移到新架构:

如果您是库维护者,请首先测试您的库是否与互操作层协同工作。如果不能,请在新架构工作组(New Architecture Working Group)上提交 issue。

为了完全支持新架构,我们建议您尽快将库迁移到新的原生模块和原生组件 API。这将允许您的库的用户充分利用新架构并支持并发特性。

您可以按照这些指南将您的模块和组件迁移到新架构

退出

如果由于任何原因,新架构在您的应用程序中运行不正常,您可以随时选择退出,直到您准备好再次启用它。

要退出新架构

  • 在 Android 上,修改 android/gradle.properties 文件并关闭 newArchEnabled 标志
-newArchEnabled=true
+newArchEnabled=false
  • 在 iOS 上,您可以通过运行命令重新安装依赖项
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install

感谢

将新架构交付给 OSS 社区是一项巨大的努力,花费了我们数年的研究和开发。我们想借此机会感谢所有现任和前任 React 团队成员,他们帮助我们实现了这一成果。

我们也非常感谢所有与我们合作促成此事的合作伙伴。特别是,我们要特别鸣谢:

  • Expo,感谢他们早期采用新架构,并支持迁移最受欢迎的库的工作。
  • Software Mansion,感谢他们维护生态系统中至关重要的库,早期将它们迁移到新架构,以及在调查和修复各种问题方面提供的所有帮助。
  • Callstack,感谢他们维护生态系统中至关重要的库,早期将它们迁移到新架构,以及对 Community CLI 工作提供的支持。
  • Microsoft,感谢他们为 react-native-windowsreact-native-macos 以及其他几个开发者工具添加了新架构的实现。
  • Expensify, Kraken, BlueSkyBrigad,感谢他们率先采用新架构并报告各种问题,以便我们能够为所有人修复这些问题。
  • 感谢所有独立的库维护者和开发者,他们通过测试新架构、修复一些问题以及对不明确的事项提出疑问来为新架构做出贡献,以便我们能够澄清这些问题。