React Native的新架构:同步和异步渲染

**作者:Emmanuel John✏️**

React Native 刚刚随着新架构的发布获得了显著的性能提升。

新架构现已成为新安装的默认架构,解决了长期以来对速度和效率的抱怨。如果您使用的是 React Native 0.76 或最新版本,这些增强功能已经可用,现在是探索这对您的项目意味着什么的激动人心的时刻。

React Native 的新架构具有更好的性能、改进的开发人员体验以及与 React 的现代功能的一致性。

本文将探讨使用新架构进行同步和异步渲染的实际用例。我们还将创建性能基准来比较新旧架构。

以下是阅读本文之前需要满足的一些先决条件:

  • 已安装 Node.js ≥v20
  • React 知识
  • 使用 React Native 构建应用程序的经验
  • 什么是新架构?

    新架构是对 React Native 内部系统的重新设计,以解决遗留架构中遇到的挑战。它支持异步和同步更新。

    传统上,React Native 依靠桥接来连接 JavaScript 和原生代码。虽然这种方法效果很好,但会带来开销。现在,新架构移除了 JavaScript 和原生代码之间的异步桥接,代之以 JavaScript 接口 (JSI)。它可以直接调用原生 C、C++ 或 Kotlin 代码(在 Android 上),而无需桥接。这允许 JavaScript 和原生层之间共享内存,从而显著提高性能。

    当与静态 Hermes 等将 JavaScript 编译为程序集的技术结合使用时,React Native 可以创建速度极快的应用程序。

    旧架构的常见问题之一是渲染初始布局和进一步更新布局之间的中间状态或视觉跳跃的可见性。

    新架构中的关键变化包括同步布局更新、并发渲染、JavaScript 接口 (JSI) 以及对高级 React 18+ 功能(如悬念转换、自动批处理和 `useLayoutEffect`)的支持。

    它还实现了与针对旧架构的库的向后兼容性。

    建立新的架构

    React Native 0.76 或最新版本默认搭载新架构。如果您使用 Expo,Expo SDK 52 现在支持 React Native 0.76。

    如果你需要在旧代码库中引入新架构,React Native Upgrade Helper 是一个有用的工具,它可以轻松地将 React Native 代码库从一个版本迁移到另一个版本:

    react native upgrade helper

    您需要做的就是输入您当前的 `react-native` 版本和您想要升级到的版本。然后您将看到对代码库进行的必要更改。

    要退出 **Android** 的新架构:

  • 打开 android/gradle.properties 文件
  • 将 newArchEnabled 标志从 true 切换为 false
  • //gradle.properties 
        +newArchEnabled=false

    要退出 **iOS** 上的新架构:

  • 打开 ios/Podfile 文件
  • 在 Podfile 的主范围内添加 ENV['RCT_NEW_ARCH_ENABLED'] = '0'(引用模板中的 Podfile): + ENV['RCT_NEW_ARCH_ENABLED']= '0' require Pod::Executable.execute_command('node', ['-p', 'require.resolve)
  • 使用以下命令安装 CocoaPods 依赖项:bundle exec pod install
  • 要了解 React Native 中的异步和同步渲染,您应该熟悉。

    异步布局和效果

    旧架构最常见的问题之一是布局更改期间的视觉故障。这是因为开发人员需要使用异步“onLayout”事件来读取视图的布局信息(这也是异步的)。这导致至少一个框架在读取和更新之前呈现了错误的布局。

    新架构通过允许同步访问布局信息并确保正确安排更新来解决此问题。这样,用户永远不会看到任何中间状态。

    为了体验新架构提供的性能和用户体验的改进,我们将使用旧架构构建自适应工具提示来体验视觉故障。

    在下一节中,我们将使用新架构构建相同的内容。您将看到工具提示将完美对齐,而无需中间状态跳转,这解决了导致用户体验不佳的视觉故障问题。

    项目设置

    确保已配置 React Native 环境。如果尚未配置,请查看 React Native CLI 快速入门指南。

    在项目文件夹中运行以下命令:

    npx react-native init ToolTipApp
    cd ToolTipApp

    运行应用

    启动 Metro 服务器:

    npx react-native start

    打开另一个终端并运行:

    npx react-native run-android

    或者:

    npx react-native run-ios

    辅助函数

    我们将实现两个辅助函数来根据以下内容计算工具提示的 x 和 y 位置:

  • 工具提示(toolTip)的尺寸和位置
  • 目标元素(target)
  • 根视图(rootView)的边界
  • 在 `src` 目录中,创建一个 `utils` 文件夹。在其中添加一个名为 `helper.js` 的新文件并包含以下代码:

    export function calculateX(toolTip, target, rootView) {
      let toolTipX = target.x + target.width / 2 - toolTip.width / 2; 
      if (toolTipX < rootView.x) {
        toolTipX = target.x; 
      }
      if (toolTipX + toolTip.width > rootView.x + rootView.width) {
        toolTipX = rootView.x + rootView.width - toolTip.width; 
      }
      return toolTipX - rootView.x; 
    }
    
    export function calculateY(toolTip, target, rootView) {
      let toolTipY = target.y - toolTip.height; 
      if (toolTipY < rootView.y) {
        toolTipY = target.y + target.height; 
      }
      return toolTipY - rootView.y;
    }

    我们还将创建另一个用于人为延迟的辅助函数:

    function wait(ms) {
      const end = Date.now() + ms;
      while (Date.now() < end);
    }

    根据位置动态造型

    我们将创建另一个辅助函数“getStyle”,它返回每个工具提示位置的适当对齐样式:

    function getStyle(position) {
      switch (position) {
        case 'top-left':
          return { justifyContent: 'flex-start', alignItems: 'flex-start' };
        case 'center-center':
          return { justifyContent: 'center', alignItems: 'center' };
        case 'bottom-right':
          return { justifyContent: 'flex-end', alignItems: 'flex-end' };
        default:
          return {};
      }
    }

    ToolTip 组件

    `ToolTip` 组件在布局后异步测量其尺寸(`rect`),并使用 `calculateX` 和 `calculateY` 函数动态更新其位置。

    在 `src` 目录中,创建一个 `components` 文件夹。在其中添加一个名为 `ToolTip.jsx` 的新文件并包含以下代码:

    import * as React from 'react';
    import {View} from 'react-native';
    import {calculateX, calculateY} from '../utils/helper'
    
    function ToolTip({ position, targetRect, rootRect, children }) {
      const ref = React.useRef(null);
      const [rect, setRect] = React.useState(null);
    
      const onLayout = React.useCallback(() => {
        ref.current?.measureInWindow((x, y, width, height) => {
          setRect({ x, y, width, height });
        });
      }, []);
    
      let left = 0;
      let top = 0;
    
      if (rect && targetRect && rootRect) {
        left = calculateX(rect, targetRect, rootRect); 
        top = calculateY(rect, targetRect, rootRect); 
      }
    
      return (
        
          {children}
        
      );
    }

    我们使用 `ref` 来存储对 `View` 元素的引用,这样我们就可以测量其在屏幕上的尺寸和位置。每当 `View` 的布局发生变化时,就会触发 `onLayout` 回调。在此回调中,`measureInWindow` 方法会检索工具提示的 `x`、`y`、`width` 和 `height`,然后将其存储在 `rect` 状态中。

    目标组件

    `Target` 组件测量其尺寸并将其传递给 `ToolTip` 组件。

    在 components 目录中,添加一个名为 Target.jsx 的新文件并包含以下代码:

    import * as React from 'react';
    import {Pressable, Text, View} from 'react-native';
    import ToolTip from './ToolTip'
    
    function Target({ toolTipText, targetText, position, rootRect }) {
      const targetRef = React.useRef(null);
      const [rect, setRect] = React.useState(null);
    
      const onLayout = React.useCallback(() => {
        targetRef.current?.measureInWindow((x, y, width, height) => {
          setRect({ x, y, width, height });
        });
      }, []);
    
      return (
        <>
          
            {targetText}
          
          
            {toolTipText}
          
        
      );
    }

    我们使用“useCallback”来获取视图的测量值,然后根据视图的位置更新工具提示的位置。

    演示组件

    该组件每秒动态更新“目标”组件的工具提示的位置,旋转不同的工具提示位置,并测量根视图尺寸以计算相对工具提示位置。

    在 components 目录中,添加一个名为 Demo.jsx 的新文件,并包含以下代码:

    import * as React from 'react';
    import {Text, View} from 'react-native';
    import Target from './Target'
    
    export function Demo() {
      const positions = ['top-left', 'top-right', 'center-center', 'bottom-left', 'bottom-right'];
      const [index, setIndex] = React.useState(0);
      const [rect, setRect] = React.useState(null);
      const ref = React.useRef(null);
    
      React.useEffect(() => {
        const interval = setInterval(() => {
          setIndex((prevIndex) => (prevIndex + 1) % positions.length); 
        }, 1000);
        return () => clearInterval(interval);
      }, []);
    
      const onLayout = React.useCallback(() => {
        ref.current?.measureInWindow((x, y, width, height) => {
          setRect({ x, y, width, height });
        });
      }, []);
    
      const position = positions[index];
      const style = getStyle(position);
    
      return (
        <>
          Position: {position}
          
            
          
        
      );
    }

    在 `useEffect` Hook 中,我们设置了一个间隔,每秒增加位置索引一次,当到达数组末尾时重置它。我们还将一个 `ref` 附加到根 `View` 容器,并在 `onLayout` 回调中使用 `measureInWindow` 方法来捕获根容器的 `x`、`y`、`width` 和 `height`。此信息存储在 `rect` 状态中并传递给 `Target` 组件,使其能够相对于根容器定位其工具提示。

    您的演示组件应如下所示:

    注意工具提示的移动和目标组件之间的时间差。这就是视觉故障。为了获得更好的用户体验,两个组件应该同时移动。

    同步布局和效果

    通过同步访问布局信息和适当安排更新,我们完全可以避免视觉故障问题,这样用户就看不到中间状态。

    通过新的架构,我们可以使用 `[useLayoutEffect](https://react.dev/reference/react/useLayoutEffect)` Hook 在一次提交中同步测量和应用布局更新,避免视觉上的“跳跃”。

    ToolTip 组件

    该组件根据“targetRect”、“rootRect”和其自身的尺寸动态定位工具提示:

    export function ToolTip({position, targetRect, rootRect, children}) {
      const ref = React.useRef(null);
      const [rect, setRect] = React.useState(null);
    
      React.useLayoutEffect(() => {
        wait(200); // Simulate delay
        setRect(ref.current?.getBoundingClientRect());
      }, [setRect, position]);
    
      let left = 0, top = 0;
      if (rect && targetRect && rootRect) {
        left = calculateX(rect, targetRect, rootRect);
        top = calculateY(rect, targetRect, rootRect);
      }
    
      return (
        
          {children}
        
      );
    }

    在 `useLayoutEffect` Hook 中,我们模拟延迟(使用 `wait` 函数),然后通过在引用的 `View` 元素上调用 `getBoundingClientRect()` 来更新工具提示的位置。此信息存储在 `rect` 状态中,并用于使用 `calculateX` 和 `calculateY` 函数计算工具提示相对于目标元素和根容器的位置。

    目标组件

    这表示目标元素并相对于其自身呈现工具提示。它使用“getBoundingClientRect”计算其尺寸。

    function Target({toolTipText, targetText, position, rootRect}) {
      const targetRef = React.useRef(null);
      const [rect, setRect] = React.useState(null);
    
      React.useLayoutEffect(() => {
        wait(200); // Simulate delay
        setRect(targetRef.current?.getBoundingClientRect());
      }, [setRect, position]);
    
      return (
        <>
          
            {targetText}
          
          
            {toolTipText}
          
        
      );
    }

    我们使用 `useRef` 创建对目标元素 (`targetRef`) 的引用,并使用 `useState` 存储其尺寸和位置 (`rect`)。在 `useLayoutEffect` Hook 中,我们使用 `wait` 函数模拟延迟,然后通过在目标元素上调用 `getBoundingClientRect()` 来更新 `rect` 状态以捕获其位置和大小。

    演示组件

    该组件通过每秒循环预定义的位置来演示工具提示的动态重新定位:

    function Demo() {
      const toolTipText = 'This is the tooltip';
      const targetText = 'This is the target';
      const ref = React.useRef(null);
      const [index, setIndex] = React.useState(0);
      const [rect, setRect] = React.useState(null);
    
      React.useEffect(() => {
        const setPosition = setInterval(() => {
          setIndex((index + 1) % positions.length); // Cycle positions
        }, 1000);
    
        return () => clearInterval(setPosition);
      }, [index]);
    
      const position = positions[index];
      const style = getStyle(position);
    
      React.useLayoutEffect(() => {
        wait(200);
        setRect(ref.current?.getBoundingClientRect());
      }, [setRect, position]);
    
      return (
        <>
          Position: {position}
          
            
          
        
      );
    }

    我们初始化一个状态变量“index”来跟踪当前位置,该变量每秒使用“useEffect”钩子中的“setInterval”循环遍历“positions”数组。使用“getStyle”函数更新“position”并用于计算根“View”容器的布局样式。

    `useLayoutEffect` Hook 用于在模拟延迟后捕获根容器 (`ref`) 的尺寸和位置,并将信息存储在 `rect` 状态中。然后,将此 `rect` 传递给 `Target` 组件,以相对于根容器定位工具提示。

    您的演示组件应如下所示:

    新架构性能基准

    React Native 团队创建了一个应用程序,将各种性能场景集中到一个地方。该应用程序可以更轻松地比较新旧架构,并识别新架构中的任何性能差距。

    在本节中,我们将构建并运行基准测试来评估新旧架构之间的性能差异。

    首先,运行以下命令来克隆应用程序:

    git clone --branch new-architecture-benchmarks https://github.com/react-native-community/RNNewArchitectureApp

    然后,安装依赖项:

    cd RNNewArchitectureApp/App
    yarn install

    运行以下命令来配置项目以使用此新架构:

    RCT_NEW_ARCH_ENABLED=1 npx pod-install

    导航到“ios”目录:

    cd ios

    打开“MeasurePerformance.xcworkspace”。按“CMD + I”进行优化构建或按“CMD + R”进行调试构建。

    对于 Android,运行以下命令来构建优化的应用程序:

    yarn android --mode release

    您还可以运行“yarn android”以调试模式构建应用程序。

    您的运行应用程序应如下所示:

    Old Architecture Running AppOld Architecture Time For Running

    单击每个按钮即可查看渲染相应组件需要多长时间。

    接下来,切换到**新架构**选项卡,重复该过程,并比较结果。

    以下是我的结果比较:

    虚拟模拟器:Google Pixel 5 API 33

    结论

    在本文中,我们通过实际用例探索了 React Native 中的同步和异步渲染,并比较了新旧架构的性能。通过基准测试结果,我们可以看到采用这种新架构的显著优势。如果您使用的是 React Native 0.76 或更高版本,则新架构已受支持且开箱即用,无需额外配置。

    LogRocket:立即重现 React Native 应用中的问题

    Instantly recreate issues in your React Native apps

    LogRocket 是一种 React Native 监控解决方案,可帮助您立即重现问题、确定错误的优先级并了解 React Native 应用程序中的性能。

    LogRocket 还可以通过向您展示用户与您的应用的互动方式来帮助您提高转化率和产品使用率。LogRocket 的产品分析功能可以揭示用户未完成特定流程或未采用新功能的原因。

    开始主动监控您的 React Native 应用程序——免费试用 LogRocket。