# React Native Turbo Starter:高效加速移动应用开发的创新框架

在移动应用开发中,React Native(RN)凭借“一次编写,多端运行”的理念大受欢迎。然而,随着项目规模增大、性能要求提升,传统的 RN 脚本生成器和 boilerplate 方案往往无法满足“零配置即开箱”以及“高性能”两大需求。为此,社区推出了 **React Native Turbo Starter**——一个集成了 Fabric 渲染管线、TurboModules、Hermes 引擎等现代化技术的全新脚手架,旨在帮助开发者快速搭建高效、可扩展、易维护的移动应用基础架构。

本文将从以下几个方面深度解读 **Turbo Starter**:  

1. [框架概述与核心特性](#一-框架概述与核心特性)  
2. [快速上手:安装与初始化](#二-快速上手安装与初始化)  
3. [核心技术解析:Fabric、TurboModules、Hermes](#三-核心技术解析fabric-turbomodules-hermes)  
4. [项目目录与模块组织](#四-项目目录与模块组织)  
5. [示例代码:快速创建首页与自定义 TurboModule](#五-示例代码快速创建首页与自定义-turbomodule)  
6. [架构图解与数据流示意](#六-架构图解与数据流示意)  
7. [性能优化与实战建议](#七-性能优化与实战建议)  
8. [总结与拓展](#八-总结与拓展)  

---

## 一、框架概述与核心特性

**React Native Turbo Starter**(以下简称“Turbo Starter”)并非普通的 `react-native init` 模板,而是一整套工程化、性能与开发体验兼顾的脚手架。其主要特性包括:  

1. **一键启用 Fabric 渲染管线**  
   - 默认集成 Fabric,自动配置 C++ 层 Shadow Tree、Yoga 布局与 JSI 绑定,极大提升渲染性能与并发能力。  

2. **TurboModules 支持**  
   - 内置 TurboModule 框架,JS 端通过 JSI 直接调用原生模块,无需 JSON 序列化,调用延迟大幅降低。  
   - 提供自动化生成模板,让你只需少量配置即可新增原生模块。  

3. **Hermes 引擎优化**  
   - 默认开启 Hermes,兼容 Android 与 iOS,让 JS 代码启动更快、内存占用更低。  
   - 内置性能剖析脚本,可在调试阶段自动生成采样报告。  

4. **TypeScript + ESLint + Prettier 预配置**  
   - 全面支持 TypeScript,结合 `tsconfig.json`、`eslint`、`prettier` 等最佳实践配置,保证代码一致性。  
   - 自带常见规则:React Hooks 检测、导入排序、变量命名规范等,让团队协作更高效。  

5. **React Navigation v6 与底层架构融合**  
   - 内置 `@react-navigation/native`、`@react-navigation/stack`,并对 Fabric 做了适配,页面跳转更流畅。  
   - 提供示例导航结构,包含常用鉴权流程、Tab 导航、Drawer 导航。  

6. **自动化 Android / iOS 构建配置**  
   - iOS 自动安装 Pod,Android 自动配置 `gradle.properties` 与 `build.gradle`,一键打包即可发布。  
   - 支持 CI/CD 集成,预置 Fastlane 配置,可快速接入自动化发布流程。  

---

## 二、快速上手:安装与初始化

### 2.1 环境要求

- Node.js ≥ 14  
- Yarn ≥ 1.22(建议)  
- React Native CLI  
- Android Studio / Xcode  
- 全局安装 `react-native-turbo-starter-cli`(可选)

### 2.2 安装 Turbo Starter

通过 **npm** 或 **Yarn** 安装全局脚手架:

```bash
# 使用 Yarn
yarn global add react-native-turbo-starter-cli

# 或者 npm
npm install -g react-native-turbo-starter-cli
说明:脚手架包名为 react-native-turbo-starter-cli,在正式使用前需保证无网络缓存冲突。

2.3 初始化项目

执行以下命令创建一个名为 MyTurboApp 的新项目:

turbo-starter init MyTurboApp
cd MyTurboApp

脚手架会自动完成以下步骤:

  1. 克隆预置模板仓库:包含 Fabric、TurboModules、Hermes、TS、ESLint 等配置。
  2. 安装依赖:node_modulesPods(iOS)、Gradle 插件(Android)。
  3. 生成 Android app/build.gradle 与 iOS Podfile 中与 TurboModules / Fabric 相关配置。
  4. 配置 tsconfig.json.eslintrc.jsprettier.config.js 等。
  5. package.json 中添加常用脚本:

    • yarn android:编译并运行 Android 模拟器 / 设备。
    • yarn ios:编译并运行 iOS 模拟器 / 设备。
    • yarn lint:执行 ESLint 检查。
    • yarn type-check:执行 TypeScript 类型检查。
    • yarn start:perspective:启动 Hermes 采样剖析模式。

初始化成功示例

✔ Cloned template repository
✔ Installed npm dependencies
✔ Installed iOS Pods
✔ Configured Android Gradle settings
✔ Initialized TypeScript & ESLint config
Project “MyTurboApp” has been created successfully!

三、核心技术解析:Fabric、TurboModules、Hermes

要真正理解 Turbo Starter 的优势,必须先掌握其底层技术栈:Fabric 渲染管线TurboModules 以及 Hermes 引擎。

3.1 Fabric 渲染管线

Fabric 是 RN 0.62+ 引入的全新渲染架构,将 Shadow Tree 与布局逻辑下沉到 C++ 层,带来更低延迟与并发渲染能力。Turbo Starter 已预先为你配置好 Fabric 环境,以下是核心流程示意:

JS 线程                        C++ 层 (Fabric)                      原生 UI 线程
│                                 │                                  │
│  React Fiber Reconciler          │                                  │
│  - Diff 新旧组件树                 │                                  │
│  - 生成布局指令、UI 操作队列          │                                  │
│    (JS 调用 FabricUIManager.jsi)  │                                  │
│─────────────────────────────────▶│                                  │
│                                 │  Fabric C++ Shadow Tree 渲染           │
│                                 │  - 构建 & 更新 ShadowNode               │
│                                 │  - 调用 Yoga 计算布局                   │
│                                 │  - 生成 View 操作列表                    │
│                                 │────────────────────────────────▶│
│                                 │                                  │  原生 队列入栈
│                                 │                                  │  - createView / updateView / removeView
│                                 │                                  │  - Layout & 绘制
│                                 │                                  │
│                                 │◀──────────────────────────────── │
│  JS 线程可并发调度 (Concurrent)    │                                  │
  • ShadowNode C++ 实现:Fabric 下的 WebUISingleThreadComponentDescriptorConcreteComponentDescriptor 等在 C++ 中维护组件树状态。
  • JSI 绑定:Turbo Starter 通过 @react-native/fabric 包将 FabricUIManager 作为 Host Object 暴露给 JS,使得 updatePropsdispatchCommand 等调用在 V8/JSI 上下文中同步执行。
  • 布局与渲染:一旦 Shadow Tree 建立或更新,C++ 层会调用 Yoga 计算 x, y, width, height,然后生成最小化的 “UI Block” 列表,直接下发给原生端,减少了 JSON 序列化。

示例:JS 端调用 Fabric 更新

import { FabricUIManager } from '@react-native/fabric';

// 创建一个新的 ShadowNode
const tag = FabricUIManager.createNode(
  'View',          // hostType
  { style: { flex: 1, backgroundColor: '#FFF' } }, // props
  0,               // surfaceId (根组件 id)
);

// 更新属性
FabricUIManager.updateNode(
  tag,
  { style: { flex: 1, backgroundColor: '#F00' } }, // 新 props
);

// 提交变更
FabricUIManager.dispatchViewUpdates(surfaceId);

3.2 TurboModules

在旧架构中,RN 通过 Bridge(JSON 序列化/反序列化)调用原生模块,开销较大。TurboModules 则使用 JSI HostObject/HostFunction,将原生模块“直接挂到 JS 引擎”上,省去了多次上下文切换。

  • JSI HostObject:一旦应用启动,Turbo Starter 会自动注册所有实现了 TurboModule 接口的原生类,将其绑定到 global.TurboModules 对象下。
  • 按需加载:首次调用 NativeModules.MyModule.doSomething() 时,Turbo Starter 会在 C++ 层加载并实例化该模块,其后调用直接通过指针访问,极大降低调用延迟。
  • 示例:创建自定义 TurboModule(以 Android 为例)

    1. 实现 Java 接口

      // android/app/src/main/java/com/myapp/MyTurboModule.java
      package com.myapp;
      
      import com.facebook.react.bridge.ReactApplicationContext;
      import com.facebook.react.turbomodule.core.interfaces.TurboModule;
      import com.facebook.react.turbomodule.core.interfaces.ReactModule;
      import javax.annotation.Nonnull;
      
      @ReactModule(name = MyTurboModule.NAME)
      public class MyTurboModule implements TurboModule {
        public static final String NAME = "MyTurboModule";
        private ReactApplicationContext reactContext;
      
        public MyTurboModule(@Nonnull ReactApplicationContext reactContext) {
          this.reactContext = reactContext;
        }
      
        @ReactMethod(isBlockingSynchronousMethod = true)
        public String getDeviceNameSync() {
          return android.os.Build.MODEL;
        }
      
        @ReactMethod
        public void showToast(String msg) {
          Toast.makeText(reactContext, msg, Toast.LENGTH_SHORT).show();
        }
      }
    2. 配置 MyTurboModulePackage

      // android/app/src/main/java/com/myapp/MyTurboModulePackage.java
      package com.myapp;
      
      import com.facebook.react.turbomodule.core.TurboReactPackage;
      import com.facebook.react.turbomodule.core.interfaces.TurboModule;
      import java.util.Collections;
      import java.util.List;
      
      public class MyTurboModulePackage extends TurboReactPackage {
        @Override
        public List<TurboModule> getModules(ReactApplicationContext reactContext) {
          return Collections.<TurboModule>singletonList(new MyTurboModule(reactContext));
        }
      
        @Override
        public ReactModuleInfoProvider getReactModuleInfoProvider() {
          return new ReactModuleInfoProvider() {
            @Override
            public Map<String, ReactModuleInfo> getReactModuleInfos() {
              final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
              moduleInfos.put(
                MyTurboModule.NAME,
                new ReactModuleInfo(
                  MyTurboModule.NAME,
                  "MyTurboModule",
                  false, // canOverrideExistingModule
                  false, // needsEagerInit
                  true,  // hasConstants
                  false, // isCxxModule
                  true   // isTurboModule
                )
              );
              return moduleInfos;
            }
          };
        }
      }
    3. 注册到 MainApplication.java

      // android/app/src/main/java/com/myapp/MainApplication.java
      @Override
      protected List<ReactPackage> getPackages() {
        List<ReactPackage> packages = new PackageList(this).getPackages();
        // 添加 TurboModulePackage
        packages.add(new MyTurboModulePackage());
        return packages;
      }
    4. JS 端调用示例

      // App.tsx
      import React, { useEffect } from 'react';
      import { Button, View, Text } from 'react-native';
      import { NativeModules } from 'react-native';
      
      const { MyTurboModule } = NativeModules;
      
      export default function App() {
        const [deviceName, setDeviceName] = useState<string>('');
      
        useEffect(() => {
          // 同步调用
          const name = MyTurboModule.getDeviceNameSync();
          setDeviceName(name);
        }, []);
      
        return (
          <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
            <Text>设备名称:{deviceName}</Text>
            <Button
              title="显示 Toast"
              onPress={() => MyTurboModule.showToast('Hello from TurboModule!')}
            />
          </View>
        );
      }

通过上面步骤,我们实现了一个同步(阻塞)方法 getDeviceNameSync 与异步方法 showToast。JS 侧无须等待异步回调即可同步获取设备名称,提升了性能与开发体验。

3.3 Hermes 引擎

Hermes 是 Facebook 针对 React Native 优化的轻量 JavaScript 引擎,启动快、内存占用低。Turbo Starter 默认在 Android 和 iOS 上启用 Hermes:

# android/app/build.gradle
project.ext.react = [
+  enableHermes: true,  // 启用 Hermes
  entryFile: "index.js"
]
  • 优势

    • 首包启动时间比 JSC 快约 30%–40%。
    • 生成的 Hermes Bytecode 可以通过 hermesc 预编译,减少运行时解析成本。
    • 较低的内存占用,适合低端设备。
  • 调试与剖析

    • Turbo Starter 中集成了 hermes-engine Profiler,只需运行:

      yarn start:perspective

      即可生成 .cpuprofile 文件,使用 Chrome DevTools 打开进行深度性能剖析。


四、项目目录与模块组织

初始化完成后,Turbo Starter 会生成如下目录结构(精简示例):

MyTurboApp/
├── android/               # 原生 Android 项目
│   ├── app/
│   ├── build.gradle
│   └── settings.gradle
├── ios/                   # 原生 iOS 项目
│   ├── Pods/
│   ├── MyTurboApp.xcworkspace
│   └── MyTurboApp.xcodeproj
├── src/
│   ├── assets/            # 图片、icon、字体等静态资源
│   ├── components/        # 通用 UI 组件
│   │   ├── Button.tsx
│   │   └── Header.tsx
│   ├── navigation/        # React Navigation 配置
│   │   ├── AppNavigator.tsx
│   │   └── screens.ts
│   ├── modules/           # 与 TurboModules 绑定的 JS 接口封装
│   │   └── MyTurboModule.ts
│   ├── screens/           # 各功能页面
│   │   ├── HomeScreen.tsx
│   │   └── DetailScreen.tsx
│   ├── services/          # 网络请求、API 封装
│   │   └── api.ts
│   ├── stores/            # 状态管理 (Redux / MobX / Recoil 等)
│   │   └── UserStore.ts
│   ├── styles/            # 公共样式、主题配置
│   │   └── theme.ts
│   ├── utils/             # 工具函数
│   │   └── helpers.ts
│   ├── App.tsx            # 入口组件,挂载 NavigationContainer
│   └── index.js           # 应用注册入口
├── .eslintrc.js           # ESLint 配置
├── tsconfig.json          # TypeScript 配置
├── babel.config.js        # Babel 配置,包含 Fabric & TurboModules 插件
└── package.json
  • components/:存放应用通用的 UI 组件,皆使用纯函数或 React.memo 进行优化。
  • navigation/:使用 React Navigation v6,AppNavigator.tsx 定义 Stack.NavigatorTab.NavigatorDrawer.Navigator 等。
  • modules/:在 TS 中为每个 TurboModule 创建类型定义与 JS 接口封装,方便在业务代码中调用。
  • stores/:可根据团队技术选型使用 Redux、MobX 或 Recoil等,一切基于 TypeScript 严格类型。
  • services/:集中封装网络请求、业务 API 与缓存逻辑,供屏幕或组件调用。
  • styles/:定义主题色、字体大小、间距统一规范,便于应对深色模式等需求。

五、示例代码:快速创建首页与自定义 TurboModule

下面以一个最常见的“Home → Detail”导航示例,以及如何在页面中调用自定义 TurboModule(如上文的 MyTurboModule)来展示数据。

5.1 配置导航

首先,安装 React Navigation 及其依赖(Turbo Starter 已预置,仅供参考):

yarn add @react-navigation/native @react-navigation/stack
yarn add react-native-screens react-native-safe-area-context

创建 src/navigation/AppNavigator.tsx

// src/navigation/AppNavigator.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import DetailScreen from '../screens/DetailScreen';

export type RootStackParamList = {
  Home: undefined;
  Detail: { itemId: number; name: string };
};

const Stack = createStackNavigator<RootStackParamList>();

export default function AppNavigator() {
  return (
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions={{
        headerTitleAlign: 'center',
        headerStyle: { backgroundColor: '#6200EE' },
        headerTintColor: '#FFF',
      }}
    >
      <Stack.Screen name="Home" component={HomeScreen} options={{ title: '首页' }} />
      <Stack.Screen name="Detail" component={DetailScreen} options={{ title: '详情页' }} />
    </Stack.Navigator>
  );
}

src/App.tsx 中挂载:

// src/App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import AppNavigator from './navigation/AppNavigator';

export default function App() {
  return (
    <NavigationContainer>
      <AppNavigator />
    </NavigationContainer>
  );
}

5.2 创建 HomeScreen

// src/screens/HomeScreen.tsx
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useNavigation } from '@react-navigation/native';
import { NativeModules } from 'react-native';

type HomeScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Home'>;

export default function HomeScreen() {
  const navigation = useNavigation<HomeScreenNavigationProp>();
  const { MyTurboModule } = NativeModules;

  const handleNavigate = () => {
    navigation.navigate('Detail', { itemId: 42, name: 'Turbo Starter' });
  };

  const handleGetDeviceName = () => {
    // 调用同步 TurboModule 方法
    const deviceName = MyTurboModule.getDeviceNameSync();
    alert(`设备名称:${deviceName}`);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>欢迎使用 Turbo Starter 框架</Text>
      <Button title="跳转到详情页" onPress={handleNavigate} />
      <View style={styles.spacer} />
      <Button title="获取设备名称 (TurboModule)" onPress={handleGetDeviceName} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24, textAlign: 'center' },
  spacer: { height: 16 },
});

5.3 创建 DetailScreen

// src/screens/DetailScreen.tsx
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { StackNavigationProp } from '@react-navigation/stack';

type DetailScreenRouteProp = RouteProp<RootStackParamList, 'Detail'>;
type DetailScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Detail'>;

export default function DetailScreen() {
  const route = useRoute<DetailScreenRouteProp>();
  const navigation = useNavigation<DetailScreenNavigationProp>();
  const { itemId, name } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.text}>项目编号:{itemId}</Text>
      <Text style={styles.text}>项目名称:{name}</Text>
      <View style={styles.spacer} />
      <Button title="返回上一页" onPress={() => navigation.goBack()} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
  text: { fontSize: 18, marginBottom: 12 },
  spacer: { height: 16 },
});

到此,我们已经完成了一个最基本的“首页 → 详情页”导航,并演示了如何调用自定义的 TurboModule 获取设备名称。所有导航和模块调用均在 Fabric + TurboModules + Hermes 的优化下高效运行。


六、架构图解与数据流示意

为了帮助你更直观地理解 Turbo Starter 的整体架构与数据流,下面给出一个 ASCII 图示,展示 JS 线程、C++ Fabric、TurboModules 与原生 UI 线程之间的交互。

┌─────────────────────────────────────────────────────────────────────────┐
│                             JS 线程 (Hermes)                            │
│                                                                          │
│   App.tsx (React Fiber)                                                  │
│   ├─ useEffect → MyTurboModule.getDeviceNameSync()       (Sync Call)     │
│   │   → JSI HostFunction (C++)                                            │
│   │                                                                          │
│   ├─ React Navigation (Home → Detail)                                      │
│   │   → RCNavigtor JSI 调用 FabricUIManager (创建 / 更新页面 Element)    │
│   │                                                                          │
│   └─ 普通业务逻辑 (组件状态更新) → 调用 React 更新 Fiber → 调度 Fabric 渲染  │
│                                                                          │
│                   │                     │                                 │
│     JSI 绑定 / HostFunction       JSI 绑定 / HostObject                    │
│                   ▼                     ▼                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │                    C++ 层 (Fabric + TurboModules)                     │  │
│   │                                                                      │  │
│   │  FabricUIManager                                               TurboModuleManager  │
│   │  ├─ ShadowNode 树管理                                         ├─ 管理 MyTurboModule 捆绑    │
│   │  ├─ 调用 Yoga 计算布局                                         │                           │
│   │  ├─ 生成 UI Operations (createView/updateView/removeView)   │                           │
│   │  ├─ dispatchViewUpdates 将操作列表发送给原生 UI 线程             │                           │
│   │  └─ 接收 JS 侧同步调用 (getDeviceNameSync → Android Build.MODEL) │                           │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                   │                     │                                 │
│                   │ Fabric UI Ops       │ TurboModule 返回数据            │
│                   ▼                     ▼                                 │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │                      原生 UI 线程 (Android / iOS)                      │  │
│   │  ├─ 执行 createView / updateView / removeView 操作                 │  │
│   │  ├─ 原生组件渲染 (UIView / ViewGroup)                              │  │
│   │  ├─ 接收用户触摸事件 → 传回 JS 线程 (JSI / Bridge)                  │  │
│   │  └─ 原生 TurboModule 直接暴露方法 (getDeviceNameSync、showToast)   │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
  • JS 线程:JSX 语法构建虚拟 DOM,React Fiber 调用 JSI 将更新指令直接发送给 Fabric UI Manager 或 TurboModule Manager。
  • C++ 层 (Fabric + TurboModules):Fabric 负责 ShadowNode 标记、布局计算与生成 “原生 UI 操作列表”;TurboModuleManager 维护所有原生模块实例,将自身 HostObject 暴露给 JS,支持同步与异步调用。
  • 原生 UI 线程:接收 Fabric 下发的操作列表,依次调用 createViewupdateViewremoveView;同时,原生 TurboModule 方法(如 getDeviceNameSyncshowToast)直接被 JSI 调用并返回结果。

整个链路中省略了大量 JSON 序列化开销,JS 与 C++ 在同一进程内通过共享指针通信,从而显著提升性能与响应速度。


七、性能优化与实战建议

即便 Turbo Starter 集成了最前沿的技术,开发者在实际项目中依然需注意以下几项优化建议,以确保应用在不同设备环境下都能拥有流畅体验。

7.1 合理拆分组件与避免无效重渲染

  • 使用 React.memouseMemouseCallback 减少重复渲染。
  • 对于大型列表,优先使用 FlatList 并配置 getItemLayoutwindowSizeinitialNumToRender 等属性。
  • 利用 useTransition(React 18+ 并发)将次要更新(如后台数据加载)标记为可延迟,避免阻塞用户交互。

7.2 精简原生模块调用

  • 虽然 TurboModules 使得调用原生模块非常高效,但如果在渲染循环中频繁调用,也可能造成瓶颈。
  • 尽量将一组操作合并为单次调用,例如:一次性获取多个设备信息,而不是多次同步调用。

7.3 利用 Hermes Profiler 做深度剖析

  • 运行命令:

    yarn start:perspective

    生成 .cpuprofile 文件,使用 Chrome DevTools → Performance 面板打开,查看堆栈采样结果。

  • 重点关注:JS 侧长时间运行的函数、布局与渲染瓶颈、频繁触发的 JSI 调用等。

7.4 关注内存泄漏与资源回收

  • 在使用大型图片、音视频等资源时,要及时释放引用,避免 OOM。
  • 对于定时器(setIntervalrequestAnimationFrame)的引用要在组件卸载时清除。
  • 尽量使用弱引用(WeakMap / WeakSet)管理缓存数据,避免持久化引用导致内存无法释放。

7.5 持续更新依赖与测试

  • React Native 及其底层依赖(Fabric、TurboModules、Hermes)还处于快速迭代阶段,保持脚手架依赖与 RN 版本同步。
  • 定期阅读官方 Release Notes,及时迁移已废弃 API。
  • 在 Android 与 iOS 真机上均做性能与兼容测试,尤其关注低端机型与旧系统版本。

八、总结与拓展

本文从零开始介绍了 React Native Turbo Starter,并通过代码示例与架构图解详细讲解了其中的关键技术点,包括 Fabric 渲染管线、TurboModules 调用方式、Hermes 引擎优化、项目目录组织、导航实例、原生模块封装等内容。以下几点可帮助你继续深入:

  1. 阅读 Fabric 源码

    • node_modules/react-native/ReactFabric 或 RN 源码仓库的 ReactCommon/fabric 目录下查阅 C++ 实现,了解 ShadowNode、ComponentDescriptor、EventPipeline 等细节。
  2. 研究 TurboModule 架构

    • 关注 ReactCommon/turbomodule/coreReactNativeHostgetTurboModuleManager 的生成逻辑,掌握自动生成原生模块绑定流程。
  3. 实践并发特性

    • 在 React 18 并发模式下,配合 Turbo Starter 进行 startTransitionuseDeferredValue 等 API 的实验,观察在 Fabric 管线下的渲染差异。
  4. 参与社区与贡献

    • Turbo Starter 虽然功能丰富,但仍处于快速发展阶段,欢迎提 Issue、PR 或参与文档撰写,与社区共同完善脚手架。

通过本文的示例与思路,你已经可以快速上手 Turbo Starter 框架,并创建一个具备高性能、可扩展、易维护的 React Native 项目。后续可根据自身业务需求,结合 Redux、MobX、Recoil、React Navigation 等技术栈,进一步构建完整的移动应用。

# React 调度系统 Scheduler 深度解析

在 React 中,**调度系统(Scheduler)** 是负责管理任务优先级、拆分工作并在合适时机执行的底层模块。它让 React 能够在保持界面流畅的同时,以合理的优先级顺序执行各种更新任务。本文将从 Scheduler 的核心概念、源码结构、任务优先级、工作循环(Work Loop)及常用 API 等方面进行深度解析,结合代码示例与 ASCII 图解,帮你理清它的实现逻辑与使用方式。

---

## 目录

1. [前言:为何需要调度系统](#前言为何需要调度系统)  
2. [Scheduler 核心概念](#scheduler-核心概念)  
   1. [任务优先级(Priority Levels)](#任务优先级priority-levels)  
   2. [时间切片与让出(Time Slicing & Yielding)](#时间切片与让出time-slicing--yielding)  
   3. [Callback 与 Task](#callback-与-task)  
3. [Scheduler API 及典型代码示例](#scheduler-api-及典型代码示例)  
   1. [安装与导入](#安装与导入)  
   2. [调度一个低优先级任务](#调度一个低优先级任务)  
   3. [判断是否应该让出(`shouldYieldToHost`)](#判断是否应该让出shouldyieldtohost)  
4. [Scheduler 源码结构与关键模块](#scheduler-源码结构与关键模块)  
   1. [`Scheduler.js` 主入口](#schedulersjs-主入口)  
   2. [`SchedulerHostConfig`](#schedulerhostconfig)  
   3. [优先级枚举与内部实现](#优先级枚举与内部实现)  
   4. [任务队列与环形链表](#任务队列与环形链表)  
5. [工作循环(Work Loop)深度剖析](#工作循环work-loop深度剖析)  
   1. [同步模式 Work Loop](#同步模式-work-loop)  
   2. [并发模式 Work Loop](#并发模式-work-loop)  
   3. [`performWorkUntilDeadline` 如何中断与恢复](#performworkuntildeadline-如何中断与恢复)  
6. [任务优先级调度流程图解](#任务优先级调度流程图解)  
7. [基于 Scheduler 实现简易任务调度示例](#基于-scheduler-实现简易任务调度示例)  
8. [常见误区与优化建议](#常见误区与优化建议)  
9. [总结与学习建议](#总结与学习建议)  

---

## 前言:为何需要调度系统

在传统的单线程 JavaScript 环境中,UI 渲染和业务逻辑都在同一个线程上执行。如果有一个耗时操作(如大规模数据处理、复杂的布局计算等)直接在主线程执行,就会导致界面卡顿、动画丢帧,造成用户体验下降。为了解决这一问题,React 引入了 **调度系统(Scheduler)**,负责将大任务拆分为若干小“工作单元(work unit)”,并结合浏览器空闲时间片(`requestIdleCallback` 或轮询 `postMessage`)动态切换执行。

Scheduler 的核心价值在于:

- **控制更新优先级**:不同类型的操作(用户交互、动画、数据更新)具有不同紧急程度,Scheduler 允许我们为任务标记优先级,从而先执行高优先级任务,后执行低优先级任务。
- **分片渲染(Time Slicing)**:将一个大任务拆成多个小任务,保证每段执行时间不会超过阈值,让出主线程给浏览器进行渲染与用户交互。
- **可中断与恢复**:在执行过程中,若遇到更高优先级任务到来,可暂停当前任务,待高优先级任务完成后再恢复执行,提高响应速度。

接下来我们先从核心概念讲起。

---

## Scheduler 核心概念

### 任务优先级(Priority Levels)

Scheduler 把任务优先级分为五档,源码中用常量说明:

```js
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
  1. ImmediatePriority(同步任务)

    • 优先级最高,用于需要立即执行的任务,例如 React 的同步更新(事件处理函数中的 setState)。
    • 这类任务会同步完成,不会中断。
  2. UserBlockingPriority(用户阻塞任务)

    • 比如用户点击、输入等离散事件,需要尽快响应。
    • 在并发模式下,这类任务会被优先安排。
  3. NormalPriority(普通任务)

    • 普通更新(如异步数据更新)通常属于此类。
    • 可以被更高优先级任务打断。
  4. LowPriority(低优先级任务)

    • 不急要的后台更新,例如预取数据、日志上报等。
    • 在主线程空闲时才会执行。
  5. IdlePriority(空闲任务)

    • 最低优先级,只有在页面长时间空闲(没有更高优先级任务)时才会执行。
    • 适合做缓存清理、统计埋点等“可延后”工作。

时间切片与让出(Time Slicing & Yielding)

浏览器每一帧大约有 16ms 的时间可用,当任务执行超过阈值(通常 5ms 左右)后,应让出主线程,让浏览器完成渲染、处理用户输入,再在下一帧继续执行剩余任务。Scheduler 借助以下原语实现这一逻辑:

  • requestIdleCallback / cancelIdleCallback

    • 在主线程空闲时执行回调,参数中包含 timeRemaining() 来判断剩余时间。
    • 若不支持 requestIdleCallback(如部分浏览器),Scheduler 会使用 postMessagesetTimeout 模拟实现。
  • shouldYieldToHost() / unstable_shouldYield()

    • 调用以检查当前帧是否剩余足够时间,若不足则应中断当前任务并安排下次继续。
  • 时间阈值(deadline)

    • 默认为 \~5ms,每次运行 Work Loop 时会计算开始时间与当前时间差,若超过阈值则暂停。

Callback 与 Task

Scheduler 内部维护一条优先级任务队列,每个任务用一个 CallbackNode 表示。一个任务(callback)至少包含以下属性:

type CallbackNode = {
  callback: (expirationTime: number) => any,
  priorityLevel: number,
  expirationTime: number,
  next: CallbackNode | null,
  previous: CallbackNode | null,
};
  • callback:实际要执行的函数,一旦调度到 CPU 空闲就会调用它。
  • priorityLevel:任务的优先级,决定它在队列中的排序。
  • expirationTime:任务的过期时间,若到期仍未执行,则应立即调度执行。
  • next / previous:形成一个环形双向链表,用以管理任务队列。

当我们调用 unstable_scheduleCallback(priorityLevel, callback, options) 时:

  1. 创建一个新的 CallbackNode,设置好 priorityLevelexpirationTime(默认过期时间依赖优先级)。
  2. 将该节点插入到已有任务队列的合适位置,确保链表按优先级与过期时间排序。
  3. 如果队列中不存在正在执行的工作循环(work loop),则调用 requestIdleCallback 提交对 performWorkUntilDeadline 的调度。

Scheduler API 及典型代码示例

安装与导入

如果你使用的是 React 17+,Scheduler 已包含在 React 包中;也可单独安装使用最新版本:

# React 内置(React 17 以后)
import {
  unstable_scheduleCallback as scheduleCallback,
  unstable_UserBlockingPriority as UserBlockingPriority,
  unstable_NormalPriority as NormalPriority,
  unstable_shouldYield as shouldYield,
} from 'scheduler';

# 或单独安装
npm install scheduler
# 然后导入同上

调度一个低优先级任务

下面示例演示如何调度一个低优先级任务,并在可用时逐步执行计算密集型操作,而不中断用户交互体验。

import React, { useState, useRef } from 'react';
import { Button, View } from 'react-native';
import {
  unstable_scheduleCallback as scheduleCallback,
  unstable_LowPriority as LowPriority,
  unstable_shouldYield as shouldYield,
} from 'scheduler';

export default function HeavyComputationDemo() {
  const [result, setResult] = useState(null);
  const workLoopId = useRef(null);

  // 一个模拟“重度计算”的大循环
  const heavyComputation = () => {
    let i = 0;
    const max = 1e8;
    let sum = 0;

    function work() {
      // 分片执行:每次计算 10000 次后检查是否应让出
      const chunkSize = 10000;
      for (let c = 0; c < chunkSize && i < max; c++, i++) {
        sum += Math.sqrt(i);
      }

      if (i < max && !shouldYield()) {
        // 还没到最大,并且当前帧还有空闲时间,继续执行
        workLoopId.current = scheduleCallback(LowPriority, work);
      } else if (i < max) {
        // 当前帧时间耗尽,下一帧继续
        workLoopId.current = scheduleCallback(LowPriority, work);
      } else {
        // 计算完成,更新结果
        setResult(sum);
      }
    }

    work();
  };

  return (
    <View style={{ padding: 20 }}>
      <Button title="开始重度计算" onPress={heavyComputation} />
      {result !== null && <Text>计算结果:{result}</Text>}
    </View>
  );
}

示例说明:

  1. 点击 “开始重度计算” 后,入口函数 heavyComputation 启动一个分片工作 work
  2. 每次循环固定执行 chunkSize 次(如 10,000 次),然后用 shouldYield() 判断当前帧是否剩余时间。
  3. 若时间未耗尽并且尚未完成所有循环,就通过 scheduleCallback(LowPriority, work) 安排下一批任务。
  4. 当循环完成后,将最终 sum 存入组件状态。此时即使有其他高优先级任务(如点击按钮、滚动),Scheduler 也会中断当前批次、先让高优先级任务执行。

判断是否应该让出(shouldYieldToHost

shouldYield(alias unstable_shouldYield)是 Scheduler 暴露给用户检查当前帧是否应当让出的 API,底层会调用 SchedulerHostConfig.shouldYieldToHost()。在浏览器环境下,它会基于 requestIdleCallbacktimeRemaining();在 React Native 环境下(或不支持 requestIdleCallback),会使用 postMessage 垒式触发“微任务”并监测时间差。

简化版伪代码:

let frameDeadline = 0;
let isMessageLoopRunning = false;

// 当浏览器空闲时(requestIdleCallback)或 setTimeout 触发时:
function performWorkUntilDeadline(deadline) {
  frameDeadline = deadline.timeRemaining() + getCurrentTime();
  isMessageLoopRunning = true;
  workLoopConcurrent();
  isMessageLoopRunning = false;
  // 如果未完成所有任务,再次调度 performWorkUntilDeadline
}

export function unstable_shouldYield() {
  // 当前时间超过 deadline,就应该让出
  return getCurrentTime() >= frameDeadline;
}

workLoopConcurrent 则会在每个单元执行后调用 unstable_shouldYield(),若返回 true,则中断循环并安排下一空闲时段继续。


Scheduler 源码结构与关键模块

下面让我们走近 Scheduler 的源码,剖析其目录结构与模块职责。

Scheduler.js 主入口

node_modules/scheduler/index.js(或 React 内置 react/src/ReactSharedInternals/scheduler)中,主要暴露的 API 包括:

export {
  unstable_scheduleCallback as scheduleCallback,
  unstable_cancelCallback as cancelCallback,
  unstable_shouldYield as shouldYield,
  unstable_runWithPriority as runWithPriority,
  unstable_getCurrentPriorityLevel as getCurrentPriorityLevel,
  unstable_requestPaint as requestPaint,
  unstable_now as now,
  unstable_ImmediatePriority as ImmediatePriority,
  unstable_UserBlockingPriority as UserBlockingPriority,
  unstable_NormalPriority as NormalPriority,
  unstable_LowPriority as LowPriority,
  unstable_IdlePriority as IdlePriority,
} from './scheduler';

这些函数和常量封装在 scheduler/src/forks/Scheduler.js(或同名文件)中。核心逻辑主要分为以下几类文件:

  • Scheduler.js:高层 API 定义,调用底层实现。
  • SchedulerHostConfig.*:不同环境(浏览器、React Native、Node.js)的适配配置。
  • SchedulerImplementation.*:核心算法实现,包括任务队列、调度逻辑、Work Loop 等。

SchedulerHostConfig

Scheduler 需要与宿主环境(Host)交互,比如获取当前时间、注册空闲回调、取消空闲回调、判断是否让出等。SchedulerHostConfig 定义了这些接口的默认实现,有多套实现:

  • 浏览器环境:用 requestIdleCallbackpostMessageperformance.now()
  • React Native 环境:没有 requestIdleCallback,使用 setTimeout(..., 0) 或直接基于 global.performance.now() 实现。
  • Node.js 环境:用 setImmediateprocess.hrtime 实现高精度计时与空闲回调。

以浏览器为例,简化版实现:

// SchedulerHostConfig.browser.js
export const requestHostCallback = (cb) => {
  requestIdleCallback(cb, { timeout: 1 });
};

export const cancelHostCallback = (cbID) => {
  cancelIdleCallback(cbID);
};

export const shouldYieldToHost = () => {
  // 当前时间超过预设 deadline
  return getCurrentTime() >= frameDeadline;
};

export const now = () => {
  return performance.now();
};

在 React Native 中,这些函数会映射到 setTimeoutclearTimeoutglobal.performance.now() 等实现。

优先级枚举与内部实现

Scheduler 通过一个名为 PriorityLevel 的枚举管理优先级,并根据优先级划分“过期时间”。核心常量如下:

export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

// 对应的过期延迟(毫秒)
const IMMEDIATE_PRIORITY_TIMEOUT = -1;
const USER_BLOCKING_PRIORITY_TIMEOUT = 250;
const NORMAL_PRIORITY_TIMEOUT = 5000;
const LOW_PRIORITY_TIMEOUT = 10000;
const IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // Infinity

当调用 scheduleCallback(priorityLevel, callback) 时,会计算该任务的 过期时间

const currentTime = now();
let timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
}
const expirationTime = currentTime + timeout;

然后将任务按 (expirationTime, priorityLevel) 排序后插入环形链表,这样可以保证:

  • 过期时间更早的任务优先调度。
  • 同过期时间时,优先级更高的任务先执行。

任务队列与环形链表

Scheduler 用一个双向环形链表维护所有待执行任务。伪代码如下:

let firstTask = null; // 指向链表中的第一个节点
let lastTask = null;

// 新增任务
function scheduleCallback(priorityLevel, callback) {
  const currentTime = now();
  const expirationTime = currentTime + getTimeoutForPriority(priorityLevel);
  const newTask = {
    callback,
    priorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  if (firstTask === null) {
    firstTask = lastTask = newTask;
    newTask.next = newTask.previous = newTask;
  } else {
    // 插入到链表尾部(简单示例,不按过期时间排序)
    lastTask.next = newTask;
    newTask.previous = lastTask;
    newTask.next = firstTask;
    firstTask.previous = newTask;
    lastTask = newTask;
  }

  // 如果当前没有调度回调,就安排 performWorkUntilDeadline
  if (!isSchedulerCallbackScheduled) {
    isSchedulerCallbackScheduled = true;
    requestHostCallback(performWorkUntilDeadline);
  }

  return newTask; // 可用于取消
}

在生产环境中,Scheduler 会在插入时按过期时间与优先级排序,以保证最紧急的任务先执行。


工作循环(Work Loop)深度剖析

调度器主要有两种运行模式:同步模式(Sync Work Loop)并发模式(Concurrent Work Loop)。它们都基于“拆分任务为小块、轮询执行并在合适时机让出”这一思路,但并发模式更注重中断与恢复。

同步模式 Work Loop

当调度同步任务(ImmediatePriority)时,Scheduler 不会分片中断,而是一次性执行完所有队列中同一优先级的任务。简化版伪码:

function workLoopSync() {
  while (firstTask !== null) {
    const currentTask = firstTask;
    // 先移除该任务
    removeTaskFromList(currentTask);

    // 执行任务
    currentTask.callback(currentTask.expirationTime);
    // 如果 callback 返回了一个新的 callback(未完成),则重新调度
    if (typeof currentTask.callback === 'function') {
      scheduleCallback(currentTask.priorityLevel, currentTask.callback);
    }
  }
  isSchedulerCallbackScheduled = false;
}
  • 由于同步任务优先级最高,不会调用 shouldYield,只要队列中还有任务就一直执行。
  • 如果在任务执行过程中调用了 scheduleCallback(UserBlockingPriority, someTask),也会插入队列,待当前同步循环结束后再执行。

并发模式 Work Loop

并发模式下,Scheduler 会定期调用 unstable_shouldYield() 判断当前帧时间是否耗尽,从而中断循环并安排下一空闲周期继续。主要伪码如下:

let currentDeadline = 0;

function performWorkUntilDeadline(deadline) {
  currentDeadline = deadline.timeRemaining() + now();
  isSchedulerCallbackScheduled = false;
  workLoopConcurrent();
}

function workLoopConcurrent() {
  while (firstTask !== null) {
    // 1. 如果任务已过期,则同步立即执行
    const currentTime = now();
    const currentTask = firstTask;
    if (currentTask.expirationTime <= currentTime) {
      // 过期任务同步执行
      removeTaskFromList(currentTask);
      currentTask.callback(currentTask.expirationTime);
      continue;
    }
    // 2. 非过期任务,根据优先级执行一个单元
    if (shouldYield()) {
      // 时间片用尽,安排下一轮继续
      scheduleHostCallback();
      return;
    } else {
      // 尚有时间片,执行任务
      removeTaskFromList(currentTask);
      const continuationCallback = currentTask.callback(currentTask.expirationTime);
      if (typeof continuationCallback === 'function') {
        // 如果任务没有完成,返回一个 continuation callback,重新插入队列
        scheduleCallback(currentTask.priorityLevel, continuationCallback);
      }
    }
  }
}

关键点:

  1. 过期任务立即执行:若 expirationTime <= now(),无论当前帧是否剩余时间,都同步执行。
  2. 判断是否让出:在执行非过期任务前,调用 shouldYield()。若返回 true,表明当前帧剩余时间不足,需暂时让出主线程,安排 performWorkUntilDeadline 在下一空闲时段再次执行。
  3. 任务拆分与继续:如果某个任务内部意识到自己尚未完成(例如 React 的 Fiber 单元),会返回一个“后续回调”(continuation callback),由调度器重新插入队列,下一轮继续执行。

performWorkUntilDeadline 如何中断与恢复

在浏览器中,performWorkUntilDeadlinerequestIdleCallback 调用,并传入一个 deadline 对象,包含 deadline.timeRemaining()。示意流程如下:

┌──────────────────────────────────────────────────────────────────┐
│             浏览器空闲 → requestIdleCallback(performWork)        │
└──────────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────────┐
│ performWork(deadline):                                            │
│   currentDeadline = now() + deadline.timeRemaining()              │
│   workLoopConcurrent()                                            │
│   if (firstTask !== null) { scheduleIdleCallback(performWork) }   │
└──────────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────────┐
│ workLoopConcurrent:                                               │
│   while (firstTask) {                                             │
│     if (task.expirationTime <= now()) { // 过期,立即执行 }       │
│     else if (shouldYield()) { // 时间片用尽,停止循环 }             │
│       scheduleIdleCallback(performWork); return;                   │
│     } else { // 执行一个工作单元 }                                  │
│       runTaskUnit();                                               │
│     }                                                              │
│   }                                                                │
│   // 队列空或执行完成,不再调度                                     │
└──────────────────────────────────────────────────────────────────┘
  • 中断条件shouldYield()true
  • 恢复时机:任务尚未完成时,workLoopConcurrent 内部主动调用 scheduleHostCallback(即 requestIdleCallback),把剩余任务延后到下一空闲周期执行。

任务优先级调度流程图解

为了更直观地理解 Scheduler 的任务调度流程,下面用 ASCII 图示分别说明普通任务(未过期)与过期任务的处理逻辑。

┌────────────────────────────────────────────────────────────┐
│                    调度者视角:scheduleCallback             │
│                                                              │
│  1. 调用 scheduleCallback(priority, callback)                 │
│  2. 计算 expirationTime = now() + timeoutForPriority(priority)│
│  3. 将新任务插入环形链表,按 expirationTime 排序               │
│  4. 如果没有 pendingCallback ,则调用 requestIdleCallback     │
└────────────────────────────────────────────────────────────┘

                   ↓                 ↑
                   ↓  下一空闲周期   │
                   ↓                 │

┌────────────────────────────────────────────────────────────┐
│               浏览器空闲 → 调用 performWorkUntilDeadline    │
└────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────────┐
│                 workLoopConcurrent 开始执行                │
│                                                            │
│   while (firstTask) {                                      │
│     currentTask = firstTask                                │
│     if (currentTask.expirationTime <= now()) {             │
│       // 过期任务:立即执行,无需检查 shouldYield()        │
│       removeTask(currentTask)                               │
│       currentTask.callback(currentTask.expirationTime)      │
│       continue                                              │
│     }                                                       │
│     if (shouldYield()) {      // 时间片用尽                  │
│       scheduleIdleCallback(performWorkUntilDeadline)         │
│       return                                                 │
│     }                                                       │
│     // 尚有时间片:执行一个工作单元                          │
│     removeTask(currentTask)                                 │
│     contCallback = currentTask.callback(currentTask.expirationTime) │
│     if (typeof contCallback === 'function') {               │
│       scheduleCallback(currentTask.priorityLevel, contCallback)      │
│     }                                                       │
│   }                                                         │
│                                                            │
│   // 队列空,停止调度                                       │
└────────────────────────────────────────────────────────────┘
  • 上图展示了当 performWorkUntilDeadline 被调用后的内部逻辑。
  • 过期任务会绕过 shouldYield() 检查,即使帧时间不足也要立即执行,以避免实时交互死锁。
  • 非过期任务先检查 shouldYield(),若时间片不足,则暂停当前循环并安排下一帧继续。

基于 Scheduler 实现简易任务调度示例

下面再给出一个完整的小示例,将多个不同优先级的任务添加到队列,并观察其执行顺序。

import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import {
  unstable_scheduleCallback as scheduleCallback,
  unstable_NormalPriority as NormalPriority,
  unstable_UserBlockingPriority as UserBlockingPriority,
  unstable_LowPriority as LowPriority,
  unstable_IdlePriority as IdlePriority,
} from 'scheduler';

export default function SchedulerDemo() {
  useEffect(() => {
    console.log('当前时间:', Date.now());

    // 用户阻塞任务:优先级较高
    scheduleCallback(UserBlockingPriority, () => {
      console.log('1. 用户阻塞任务执行(优先级 2)');
    });

    // 普通任务:优先级 3
    scheduleCallback(NormalPriority, () => {
      console.log('2. 普通任务执行(优先级 3)');
    });

    // 低优先级任务:优先级 4
    scheduleCallback(LowPriority, () => {
      console.log('3. 低优先级任务执行(优先级 4)');
    });

    // 空闲任务:优先级 5
    scheduleCallback(IdlePriority, () => {
      console.log('4. 空闲任务执行(优先级 5)');
    });

    // 同步任务:优先级 1,会立即执行
    scheduleCallback(ImmediatePriority, () => {
      console.log('0. 同步任务执行(优先级 1)');
    });
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>查看控制台输出,观察执行顺序</Text>
    </View>
  );
}

运行结果(Console)

当前时间:1620000000000
0. 同步任务执行(优先级 1)
1. 用户阻塞任务执行(优先级 2)
2. 普通任务执行(优先级 3)
3. 低优先级任务执行(优先级 4)
4. 空闲任务执行(优先级 5)
  • 同步任务(ImmediatePriority) 最先执行,不经过时间切片检查。
  • 随后 UserBlockingPriority 任务、NormalPriority 任务、LowPriority 任务、IdlePriority 任务按优先级依次执行。
  • 如果其中某个任务内部耗时较长,则可能会被后续更高优先级任务打断(如果它们尚未过期且到来)。

常见误区与优化建议

  1. 误区:unstable_scheduleCallback 会立即执行

    • 只有 ImmediatePriority 任务会同步执行;其余任务都需要等待浏览器空闲或下一帧时间片才会运行。
  2. 误区:shouldYield 返回 true 即不执行任何任务

    • shouldYield 只是表明当前帧剩余时间不足,如果任务已过期(expirationTime <= now()),仍会立即执行,跳过让出逻辑。
  3. 优化:合理拆分任务粒度

    • 当一个任务逻辑十分庞大(如大规模循环、复杂计算)时,应主动拆成多个子任务,避免一次执行耗时过长。
    • 使用 unstable_scheduleCallback 在循环内部分片,确保 shouldYield() 能及时生效。
  4. 优先级选取示例

    • 用户输入点击事件 等敏感交互要使用 UserBlockingPriority,避免输入迟滞。
    • 数据轮询预加载 可使用 LowPriority,不抢占重要更新的执行。
    • 日志、分析、缓存清理 等极低优先任务,用 IdlePriority,只有在完全空闲时才执行。
  5. 结合 React Concurrent Mode

    • 在 React 18+ 并发模式下,Scheduler 与 React Fiber 调度紧密结合,startTransition 会将更新标记为“可中断”的低优先级更新(通常映射到 NormalPriority)。
    • 不要在 render 中做过度阻塞主线程的操作,否则会影响并发更新的效果。

总结与学习建议

本文从以下几个方面深度解析了 React 的调度系统 Scheduler:

  • 核心概念:任务优先级、时间切片、让出逻辑。
  • 主要 APIunstable_scheduleCallbackunstable_shouldYieldPriority Levels 等。
  • 源码架构:HostConfig、内部环形链表、过期时间排序、工作循环(同步/并发模式)。
  • 典型示例:如何调度一个耗时计算并结合 shouldYield 分片执行;多优先级任务执行顺序;自定义任务调度示例。
  • 常见误区与优化建议:正确理解让出与过期任务、拆分任务粒度、结合 React 并发模式等。

要进一步掌握 Scheduler,推荐以下学习路径:

  1. 阅读官方源码

    • 仔细阅读 node_modules/scheduler/src/ 中的 Scheduler.jsSchedulerHostConfig*.jsSchedulerImplementation.js 文件。
    • 理解各个模块之间如何协作:HostConfig 提供环境 API、Implementation 负责队列与执行、Scheduler.js 暴露给用户的接口。
  2. 调试与实验

    • 在浏览器或 React Native 环境下,多写几个示例,观察不同优先级任务互相打断的行为。
    • 在 Chrome DevTools 或 React Native Debugger 中使用 Timeline/Profiler 模式,查看帧率与任务执行切片情况。
  3. 结合 React Fiber 调度

    • 阅读 React 源码中的 ReactFiberWorkLoop.js,了解 React 如何调用 Scheduler 来安排组件更新。
    • 尝试在自定义 Hook 或组件中使用 startTransition 标记“可延迟更新”,观察界面响应变化。
  4. 关注社区 RFC

    • React 团队会在 GitHub 上发布 Scheduler 相关的 RFC(例如“支持优先级更细分”或“改进时间切片”),可定期跟踪。
    • 参与社区讨论,有助于更快掌握新特性与最佳实践。

通过本文以及后续持续的源码阅读与实验,你将具备深入理解并高效使用 React 调度系统的能力,并能在复杂应用中通过合理安排任务优先级来优化性能与用户体验。

# React Native 架构与源码深度解读

React Native(以下简称 RN)凭借“一次编写,多端运行”的理念,迅速在移动开发领域崛起。本篇文章将从整体架构层面入手,逐步深入其底层源码,包括 JS 与原生桥接机制、渲染双引擎、Fabric 架构、TurboModules、架构演变和示例代码解析。文章结构如下:

1. [前言:为什么要深度解读 React Native](#前言为什么要深度解读-react-native)  
2. [整体架构概览](#整体架构概览)  
   1. [JavaScript 线程与原生线程分离](#javascript-线程与原生线程分离)  
   2. [桥接(Bridge)机制](#桥接bridge机制)  
   3. [UI 管道:从 JS 到原生视图](#ui-管道从-js-到原生视图)  
3. [旧架构:Bridge+ShadowTree 渲染流程](#旧架构bridgeshadowtree-渲染流程)  
   1. [JavaScriptCore 与 Metro Bundler](#javascriptcore-与-metro-bundler)  
   2. [Shadow Tree(阴影树)与布局计算](#shadow-tree阴影树与布局计算)  
   3. [Bridge 调用与异步队列](#bridge-调用与异步队列)  
   4. [示例:自定义 Native Module](#示例自定义-native-module)  
4. [新架构:Fabric & TurboModules](#新架构fabric--turbomodules)  
   1. [为何引入 Fabric?旧架构不足点](#为何引入-fabric旧架构不足点)  
   2. [Fabric 渲染管线核心](#fabric-渲染管线核心)  
   3. [TurboModules:更高效的原生模块访问](#turbomodules更高效的原生模块访问)  
   4. [示例:TurboModule 定义与调用](#示例turbomodule-定义与调用)  
5. [架构图解与流程示意](#架构图解与流程示意)  
6. [源码剖析重点文件与目录](#源码剖析重点文件与目录)  
   1. [`ReactAndroid/` 目录结构](#reactandroid-目录结构)  
   2. [`ReactCommon/` 目录要点](#reactcommon-目录要点)  
   3. [`React/RCTModule/` 与 `Fabric/`](#reactrctmodule-与-fabric)  
7. [架构演进与性能优化点](#架构演进与性能优化点)  
   1. [从 “Bridge-only” 到 “Turbo” 路线图](#从-bridge-only-到-turbo-路线图)  
   2. [JSC → Hermes 引擎切换](#jsc--hermes-引擎切换)  
   3. [Concurrent React 兼容与异步更新](#concurrent-react-兼容与异步更新)  
8. [React Native 常见性能调优实践](#react-native-常见性能调优实践)  
9. [总结与学习建议](#总结与学习建议)  

---

## 前言:为什么要深度解读 React Native

对 RN 源码进行深度研究,可以帮助我们:

1. **理解性能瓶颈来源**:了解 JS 和原生交互、渲染流程,有助于定位性能热点。  
2. **自定义 Native 组件**:知道桥接如何工作,才能正确编写高性能的自定义原生模块和视图组件。  
3. **紧跟架构演进**:Fabric、TurboModules、Hermes 和 Concurrent React 都在不断演进,深入源码才能快速上手并解决兼容问题。  
4. **提高开发效率**:借鉴 RN 架构设计思想,优化自己的项目结构和工程化方案。  

下面逐层剖析 RN 底层管理 JS、原生模块与 UI 渲染的关键技术细节,帮助你迅速理清全局脉络。

---

## 整体架构概览

React Native 将移动端应用分为两部分:**JavaScript 线程** 与 **原生线程**(UI 线程 + Java/Kotlin、Objective-C/Swift 层)。它们通过桥接(Bridge)进行异步通信。整体架构可分为三大模块:

1. **JS 引擎与逻辑执行**  
   - 内置 JavaScriptCore(iOS)或使用 Hermes(Android/iOS),执行用 React 语法编写的业务逻辑与 UI 声明。  
   - `Metro Bundler` 将所有 JS 模块打包成单一 `index.bundle`(或多个分块 bundle),供手机端加载。  

2. **桥接(Bridge)层**  
   - 负责在 JS 线程与原生线程之间进行消息编解码。旧架构使用 JSON 序列化,成为性能瓶颈;新架构下的 TurboModules 和 JSI(JavaScript Interface) 通过共享 C++ 对象指针,大大降低了往返开销。  

3. **UI 管道**  
   - **旧架构**:JS 计算出最终的 Shadow Node 树(阴影树),并通过 Bridge 把变更下发到原生 Shadow Tree 模块,由 Yoga 计算布局,生成具体坐标后再下发通过 UIManager 进行原生 View 的增删改操作。  
   - **新架构 Fabric**:直接在 C++ 层面管理“Shadow Tree”并使用 JSI 直接回调布局与渲染接口,整个流程更底层、更细粒度、更高效。  

下图简要展示两种架构的对比流程:  

旧架构(Bridge + Shadow Tree) 新架构(Fabric + TurboModules)
╔════════════════════════════╗ ╔════════════════════════════╗
║ JS 线程(JSC/Hermes) ║ ║ JS 线程(JSC/Hermes) ║
║ React 组件与业务逻辑 ║ ║ React 组件与业务逻辑 ║
╠════════════════════════════╣ ╠════════════════════════════╣
║ Shadow Props → JSX 元素 ║ ║ JSX 元素 → JSI 直接调用 C++ ║
║ (虚拟 DOM 树) ║ ║ Fabric Shadow Node 构建 ║
╠════════════════════════════╣ ╠════════════════════════════╣
║ Bridge(JSON 序列化) ║◀─────────▶ ║ JSI(Host Object / Host ║
║ 发送更新指令到 Native ║ ║ Function) ║
╠════════════════════════════╣ ╠════════════════════════════╣
║ Native 阴影树(Yoga 计算) ║ ║ C++ 阴影树(Yoga/新版布局) ║
║ 计算最终布局 → UIManager ║ ║ 直接在 C++ 执行布局与渲染 ║
╠════════════════════════════╣ ╠════════════════════════════╣
║ UI 线程:原生 View 操作 ║ ║ UI 线程:原生 View 操作 ║
╚════════════════════════════╝ ╚════════════════════════════╝


下面我们分章节详细说明各个模块的核心工作原理与源码实现思路。

---

## 旧架构:Bridge+ShadowTree 渲染流程

在 RN 0.60 以前,核心架构主要依赖桥接(Bridge)和 Yoga 布局(由 Facebook 开源)。这一套架构虽已逐渐被 Fabric 取代,但理解它对后续学习 Fabric 和 TurboModules 非常重要。

### JavaScriptCore 与 Metro Bundler

1. **JavaScriptCore(JSC)**  
   - RN 默认在 iOS 平台使用 iOS 系统自带的 JSC;在 Android 端原先也依赖社区提供的 JSC 库,后来可以选择使用 Hermes。  
   - JSC 会在 App 启动时将 `main.jsbundle` 加载到内存,启动一个 JS 线程。此线程执行 React 业务逻辑、Hooks、状态更新和渲染计算。  

2. **Metro Bundler**  
   - `Metro` 是 RN 的打包工具,会把各个 JS 模块打包成一个(或多个)可供原生端加载的 bundle。例如:`index.android.bundle`、`index.ios.bundle`。  
   - 支持 source maps,以便调试。  

3. **JS 线程入口**  
   - 在 iOS `AppDelegate.m` 的 `didFinishLaunching` 中,会调用:
     ```objc
     RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
     RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"YourAppName" initialProperties:nil];
     ```
   - `RCTBridge` 会加载本地或远程 `main.jsbundle`,创建一个 `JSContext`,并在其中执行 `AppRegistry.registerComponent('YourAppName', () => App);` 注册根组件。  

### Shadow Tree(阴影树)与布局计算

在旧架构中,RN 维护了两棵树:**Shadow Tree(ShadowNode)** 和真实 **View Tree(UIView/Android View)**。Shadow Tree 负责布局计算和离屏差分,最终再将变化下发真实 View 对象。

1. **ShadowNode**  
   - 对应 RN 中每个 JS 组件的“原生阴影节点”,定义在 `ReactCommon/yoga` 或 `React/CoreModules/RCTUIManager` 中。ShadowNode 只存储布局属性(如 `flex`、`margin`、`padding`、`width`、`height`)。  
   - ShadowNode 树的构建基于 JS 端调用 `React.createElement` 产生的虚拟 DOM,当 JS 调用 `setState` 或 `props` 发生变化时,新的 ShadowNode 会重新进入布局计算。  

2. **Yoga 布局引擎**  
   - Facebook 开源的跨平台布局库(基于 FlexBox 算法),通过对 ShadowNode 树执行 `YGNodeCalculateLayout`,得到每个节点最终的 `x, y, width, height`。  
   - ShadowNode 的属性会同步设置到 Yoga 中间层,为布局计算提供依据。  

3. **Bridge 下发 UI 操作**  
   - 当 ShadowNode 布局计算完成后,RN 会遍历 Shadow Tree,对比新旧布局差异,将每个需要移动、插入或删除的 ShadowNode 生成一条 “UI 编辑指令(UIOperation)” → 通过 Bridge 发送给原生端的 UIManager。  
   - UIManager 将这些指令转换为对应的 `UIView`(iOS)或 `ViewGroup`(Android)操作,例如 `createView`、`updateView`、`manageChildren`。  

4. **示例:ShadowNode 更新流程**  
   - JS 端调用 `setState` → 触发 `RCTExecuteOnJavaScriptQueue` 执行组件 render → 新旧虚拟 DOM 差分 → 调用 `UIManager.createView` / `UIManager.updateView` → ShadowNode 属性更新 → Yoga 重新计算 → 生成布局结果 → UIManager 下发真实 View 更新 → 原生 UI 线程执行相应操作。  

### Bridge 调用与异步队列

桥接层将 JS 与原生分离,通过异步消息队列通信。核心实现 resides 于:

- **iOS:`RCTBridge.mm` 与 `RCTBatchedBridge`**  
- **Android:`CatalystInstanceImpl.java` **  

1. **信息编码**  
   - 旧架构将需要传递的参数(如创建 View 时的属性字典)序列化为 JSON 数组,再通过 JNI(Android)或 Objective-C 原生函数(iOS)发送给对端。  
   - 此种方式的缺点在于 JSON 序列化开销大、内存分配频繁、跨线程上下文切换耗时。  

2. **异步队列**  
   - JS 线程将所有桥接请求(例如 `UIManager.createView(...)`、`NativeModules.YourModule.yourMethod(...)`)推入一个被称为 “Batched Bridge Queue” 的队列,定期打包成一批消息下发给原生。  
   - 原生执行完成后,可选择通过回调将结果再回传给 JS。  

3. **示例:调用 Native Module**  
   - JS 端代码:
     ```js
     import { NativeModules } from 'react-native';
     NativeModules.ToastModule.show('Hello from JS!', NativeModules.ToastModule.SHORT);
     ```
   - JS 线程将这条调用封装成 `["ToastModule", "show", ["Hello from JS!", 0]]`,放入 BatchedBridge 队列。  
   - 原生端收到 JSON 消息后,在相应 Module 中找到 `show` 方法并执行。  

### 示例:自定义 Native Module

下面演示如何在旧架构下为 Android 创建一个简单的 Toast Module。

1. **创建 Java 类 `ToastModule.java`**  
   ```java
   // android/app/src/main/java/com/yourapp/ToastModule.java
   package com.yourapp;

   import android.widget.Toast;
   import com.facebook.react.bridge.ReactApplicationContext;
   import com.facebook.react.bridge.ReactContextBaseJavaModule;
   import com.facebook.react.bridge.ReactMethod;

   public class ToastModule extends ReactContextBaseJavaModule {
     private static ReactApplicationContext reactContext;

     public ToastModule(ReactApplicationContext context) {
       super(context);
       reactContext = context;
     }

     @Override
     public String getName() {
       return "ToastModule";
     }

     @ReactMethod
     public void show(String message, int duration) {
       Toast.makeText(reactContext, message, duration).show();
     }
   }
  1. 创建 ToastPackage.java

    // android/app/src/main/java/com/yourapp/ToastPackage.java
    package com.yourapp;
    
    import com.facebook.react.ReactPackage;
    import com.facebook.react.bridge.NativeModule;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.uimanager.ViewManager;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class ToastPackage implements ReactPackage {
      @Override
      public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new ToastModule(reactContext));
        return modules;
      }
    
      @Override
      public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
      }
    }
  2. 注册到 MainApplication.java

    // android/app/src/main/java/com/yourapp/MainApplication.java
    @Override
    protected List<ReactPackage> getPackages() {
      List<ReactPackage> packages = new PackageList(this).getPackages();
      // 手动添加
      packages.add(new ToastPackage());
      return packages;
    }
  3. JS 端调用示例

    // App.js
    import React from 'react';
    import { Button, NativeModules, View } from 'react-native';
    
    export default function App() {
      const { ToastModule } = NativeModules;
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Button
            title="Show Toast"
            onPress={() => ToastModule.show('Hello from RN!', ToastModule.SHORT)}
          />
        </View>
      );
    }

这样,当你按下按钮时,JS 端通过 Bridge 将调用下发到原生,执行 Toast.makeText 显示 Android 系统 Toast。


新架构:Fabric & TurboModules

为了解决 Bridge 性能瓶颈与布局卡顿问题,React Native 社区推出了新的架构方案:Fabric 渲染管线TurboModules。自 RN 0.62 起分步引入,RN 0.65+ 已支持并逐渐默认启用。

为何引入 Fabric?旧架构不足点

  1. Bridge 性能瓶颈

    • JSON 序列化与反序列化开销大,往返耗时明显。
    • Shadow Tree → Layout → UI 操作需要多次跨桥,UI 更新延迟。
  2. 无法细粒度调度布局

    • 旧架构 ShadowNode 计算与 UI 更新合并在一起,难以“中断”或“并发处理”子树,更难适配 Concurrent React。
  3. 原生模块加载不够灵活

    • NativeModules 以“静态注册”为主,不支持按需加载,高数量原生模块时耗时且占内存。

Fabric 渲染管线核心

Fabric 架构在 C++ 层面实现了 Shadow Tree,核心思想是将布局与渲染管线下沉到 C++,并借助 JSI(JavaScript Interface)将它暴露给 JS,从而使 JS 与原生能在同一个事件循环中交互,无需通过异步 Bridge。

  1. JSI(JavaScript Interface)

    • JSI 是一套在 JS 引擎(JSC 或 Hermes)与 C++ 之间的通信接口。与旧 Bridge 不同,JSI 直接将 C++ 对象注入到 JS 全局变量中,避免 JSON 序列化/反序列化。
    • FabricUIManagerTurboModuleManager 等都通过 JSI 注册为 JS 可调用的对象(Host Object / Host Function)。
  2. Fabric Shadow Tree

    • ShadowNode 转移到 C++,并使用 Yoga(或重写的 C++ 布局引擎)计算布局。
    • JS 通过 JSI 直接调用 FabricUIManager.createNode(tag, props, rootTag) 创建 ShadowNode;
    • 当组件更新时,JS 端调用 FabricUIManager.updateNode(node, newProps),C++ 端更新布局属性并触发重排。
  3. 同步调用与并发

    • Fabric 支持 JS 与原生在同一线程进行布局与渲染交互,可灵活支持 Concurrent React。
    • 在 C++ 层使用“Reconcile on UI Thread”模式,让 UI 更新更流畅,减少卡顿。
  4. 示意流程

    JS 线程(Hermes/JSC)               C++ 层(Fabric)       原生 UI 线程
    ┌─────────────────────────────┐        ┌────────────────────┐
    │ React render() → JSX 树      │        │                    │
    │ createNode(hostType, props)  │ ──┐   │                    │
    └─────────────────────────────┘   │   │                    │
                                      │   │   Fabric Shadow Tree │
    ┌─────────────────────────────┐   │   │  layoutWithYoga()     │
    │ updateProps(node, newProps) │ ──┼──▶│  计算 x,y,width,height │
    └─────────────────────────────┘   │   └────────────────────┘
                                      │           │
                                      │           ▼
                                      │   ┌────────────────────┐
                                      │   │ UI Commands List   │
                                      │   │(e.g. createView/   │
                                      │   │ updateView/ remove)│
                                      │   └────────────────────┘
                                      │           │
                                      │           ▼
    JS 可并发调度(Concurrent Mode)    │   ┌────────────────────┐
    └─────────────────────────────┘        │    原生 UI 线程       │
                                           │  apply UI Commands   │
                                           └────────────────────┘

TurboModules:更高效的原生模块访问

TurboModules 架构将旧的 NativeModules 静态注册替换为按需加载的模块系统,主要特点如下:

  1. JSI 桥接

    • 通过 JSI 将每个原生模块(C++ / Java / Objective-C)注册为一个 Host Object,在 JS 端直接读取,这种方式无须异步 Bridge,调用更快。
  2. 按需加载

    • 模块仅在首次调用时加载,大大减少冷启动时的开销。
    • 不再需要在 MainApplication.java 中手动注册所有模块,而是遵循约定目录 ReactCommon/turbomodule/ 或 Gradle 配置自动生成。
  3. 异步与同步方法支持

    • TurboModules 支持直接返回同步值(如获取设备信息)或使用 Promise(异步)。
    • 通过 JSI 将回调和 Promise 直接挂到 JS 引擎上,无需 JSON 序列化。
  4. 示例:定义一个简单的 TurboModule
    以下以 iOS 为例,演示在 new architecture 下创建一个简单的 DeviceInfo TurboModule。

    1. Objective-C++ 接口声明

      // ios/ReactNativeNewArch/DeviceInfoModule.h
      #import <React/RCTBridgeModule.h>
      #import <ReactCommon/RCTTurboModule.h>
      #import <React/RCTMethodInfo.h>
      
      @interface DeviceInfoModule : NSObject <RCTTurboModule>
      @end
    2. 实现 .mm 文件

      // ios/ReactNativeNewArch/DeviceInfoModule.mm
      #import "DeviceInfoModule.h"
      #import <UIKit/UIKit.h>
      #import <jsi/jsi.h>
      #import <React/RCTJSIUtils.h>
      
      using namespace facebook::jsi;
      
      @implementation DeviceInfoModule
      
      RCT_EXPORT_MODULE(DeviceInfo);
      
      // 在 Fabric+TurboModules 中,推荐使用 JSINativeModules 方式
      - (std::shared_ptr<facebook::react::CallInvoker>)getCallInvoker {
        return RCTCxxBridge_getJSCallInvoker(_bridge.compilerFlags);
      }
      
      - (void)install:(facebook::jsi::Runtime &)runtime
      {
        auto deviceInfo = Object::create(runtime);
        auto getSystemVersion = Function::createFromHostFunction(
          runtime,
          PropNameID::forAscii(runtime, "getSystemVersion"),
          0,
          [](Runtime &rt, Value const &, Value const *args, size_t) -> Value {
            NSString *version = [UIDevice currentDevice].systemVersion;
            std::string ver = [version UTF8String];
            return String::createFromUtf8(rt, ver);
          }
        );
        deviceInfo.setProperty(runtime, "getSystemVersion", getSystemVersion);
        runtime.global().setProperty(runtime, "DeviceInfo", deviceInfo);
      }
      
      @end
    3. JS 端调用示例

      // 在 DevTools 中调试
      console.log(DeviceInfo.getSystemVersion()); // e.g. "14.4"

    这段代码利用 JSI 将 DeviceInfo.getSystemVersion() 直接挂载到 JS 全局,通过 C++/Objective-C++ 获取系统版本并返回,无需走 Bridge。


架构图解与流程示意

下面汇总一个较为完整的架构图,帮助你理清旧架构与新架构中各个模块的职责与调用链路。图中以箭头表示调用方向、数据流转路径。

┌─────────────────────────────────────────────────────────────────────────┐
│                             JS 线程(JSC / Hermes)                    │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                         React 应用层                              │   │
│   │   App.js → 组件树 → Hooks / Redux / 状态管理 → JSX 构建虚拟 DOM       │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│             │               │           │             │                 │
│             ▼               ▼           ▼             ▼                 │
│   ┌──────────────────┐  ┌────────┐  ┌────────────┐  ┌─────────┐           │
│   │ setState / dispatch │  │  navigate │  │ AsyncStorage │  │网络请求等│           │
│   └──────────────────┘  └────────┘  └────────────┘  └─────────┘           │
│             │                                                         │
│             ▼                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                       Fabric 管线 / 旧 Bridge                      │   │
│   │ ┌──────────────┐  ┌───────────────────┐  ┌─────────────────────┐ │   │
│   │ │ TurboModule   │  │  Bridge / JSI      │  │ UIManager / FabricUI │ │   │
│   │ │ (按需加载)    │  │ (JSON / HostObject)│  │   Shadow Nodes       │ │   │
│   │ └──────────────┘  └───────────────────┘  └─────────────────────┘ │   │
│   │       ▲                 ▲                      ▲               │   │
│   │       │                 │                      │               │   │
│   │       │  (方法调用)     │  (事件 / 更新)      │  (布局 / 渲染)  │   │
│   │       │                 │                      │               │   │
│   └───────┴─────────────────┴──────────────────────┴───────────────┘   │
│             │                 │                      │                 │
│             ▼                 ▼                      ▼                 │
│   ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐       │
│   │  Native Modules  │  │ Shadow Tree /    │  │ Layout Engine    │       │
│   │  (Java / Obj-C)  │  │ Fabric C++ 节点   │  │  (Yoga or C++   )│       │
│   └──────────────────┘  └──────────────────┘  └──────────────────┘       │
│             │                 │                      │                 │
│             ▼                 ▼                      ▼                 │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                        原生 UI 线程(iOS/Android)               │   │
│   │  ┌───────────────────────────────────────────────────────────┐ │   │
│   │  │             原生 View Hierarchy / Layer                  │ │   │
│   │  │  createView / updateView / manageChildren / removeView    │ │   │
│   │  └───────────────────────────────────────────────────────────┘ │   │
│   │         ▲                              ▲                        │   │
│   │         │                              │                        │   │
│   │         │  用户触摸事件 / 原生回调         │  Native Event 回传     │   │
│   │         │                              │                        │   │
│   └─────────┴──────────────────────────────┴────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
  • JS 线程:负责业务逻辑、Hooks、组件树、状态更新、构建 ShadowNode / Fabric 指令,并通过 Bridge/JSI 与原生交互。
  • Fabric 管线 / 旧 Bridge:连接 JS 与原生。Fabric 下移 ShadowNode 到 C++,并借助 JSI 直接回调,Bridge 下旧有 JSON 串行化方式。
  • Native 线程:管理原生 View 层级,执行增删改操作,并将触摸等事件回传给 JS(例如 TouchableOpacityonPress)。

源码剖析重点文件与目录

RN 源码庞大,但对理解架构极为重要的目录集中在以下位置:

ReactAndroid/ 目录结构

  • ReactAndroid/src/main/java/com/facebook/react/

    • CatalystInstanceImpl.java:旧架构下 Bridge 核心实现,负责注册 Module、MessageQueue、调用 JS。
    • UIManagerModule.java:管理 ShadowNode、布局计算、最终下发 View 操作给原生。
    • ReactRootView.java:RN 根视图容器,加载 JS Bundle 并初始化 Bridge。
    • Fabric 子目录:Fabric 架构相关 Android 实现,包括 FabricUIManager.javaMountingManager.java
    • TurboModule 子目录:TurboModules 相关工厂和管理器,例如 TurboModuleManager.java
  • ReactAndroid/src/main/jni/

    • 包含 C++ 层面对 JSI、Fabric、Yoga 引擎的绑定。

ReactCommon/ 目录要点

  • jsi/

    • JSI 数据类型与接口定义,例如 jsi.hValue.hRuntime.h
  • jsiexecutor/

    • JSI 在 Android 与 iOS 上的桥接实现,例如 JSCRuntimeHermesRuntime
  • fabric/

    • Fabric C++ 核心代码,包含 ShadowNode 定义、事件调度、UIManager 接口。
  • turbomodule/

    • 定义了 TurboModule ABI,C++ / Java / Obj-C++ 通过这个接口生成对应的模块绑定。
  • yoga/

    • Facebook 开源的 Yoga 布局引擎源代码。

React/RCTModule/Fabric/

  • React/RCTModule/

    • 旧版 NativeModule 注册与调用机制,包含 RCTBridgeModule.hRCTBridge.h
  • Fabric/

    • iOS 端的 Fabric 相关目录,包含 RCTFabricUITurboModuleManager.mmRCTSurfacePresenter 等。

以上目录和文件是 RN 架构的核心,深入阅读这些源码可直接理解 RN 如何从 JS 调用原生、如何执行布局、如何同步渲染。


架构演进与性能优化点

从 “Bridge-only” 到 “Turbo” 路线图

  1. Bridge-only 架构(RN 0.59 及以前)

    • JSON 串行化 → native 执行 → JSON 反序列化,异步队列。
    • ShadowTree → Yoga 布局 → UIManager。
  2. TurboModules + Fabric(RN 0.62 – 0.66)

    • 使用 JSI 替代 JSON Bridge,NativeModules 改为 TurboModules,布局在 C++ 层面。
    • JS 与 UIManager 直接在同一线程交互,可实现“同步渲染”与“部分异步渲染”。
  3. Hermes 嵌入(RN 0.64 及以后)

    • Android 默认可切换到 Hermes,性能更优,内存占用更低。
    • JSC 下仍保留,但社区推荐使用 Hermes 以配合 Fabric / TurboModules。
  4. Concurrent React 兼容(RN 0.68+)

    • 支持 React 18 中的并发特性,Fiber reconciler 与 Scheduler 调度更紧密结合。
    • Fabric 管线结合 startTransitionuseDeferredValue 等并发 API,提升复杂 UI 的渲染流畅度。

JSC → Hermes 引擎切换

  • JSC(JavaScriptCore)

    • iOS 平台默认 JSC;Android 端曾经使用 jsc-android 二进制包。
    • 缺点:启动慢、内存占用高、缺乏及时更新。
  • Hermes

    • Facebook 自研的 JavaScript 引擎,针对 RN 做了裁剪,启动速度快、内存占用低。
    • RN 0.64+ 引入 hermes-engine,可通过 enableHermes: trueandroid/app/build.gradle 中启用:

      project.ext.react = [
        enableHermes: true,  // must be true to use Hermes
      ]
    • 相较 JSC,Hermes 在首包启动时间可提升约 25%\~40%,并在动画、JS 密集计算场景更流畅。

Concurrent React 兼容与异步更新

  • React 18 提出了“并发模式”,RN 也逐步支持:

    • 在 JS 端编写组件时,可使用 startTransition(() => setState(...)) 标记“可延迟更新”,允许 UI 在用户滚动、点击等操作中保持流畅。
    • Fabric 通过底层的 Scheduler 配合 JSI 调度,能在 UI 线程与 JS 线程之间分割任务,避免长时间占用导致卡顿。

React Native 常见性能调优实践

  1. 避免过度使用 Bridge

    • 批量调用 Bridge 操作(例如 UIManager),而非频繁单条调用。
    • 旧架构下可使用 UIManager.dispatchViewManagerCommand 一次传多个命令。
  2. 使用 FlatList 优化长列表

    • FlatList 支持 windowSizeinitialNumToRendermaxToRenderPerBatch 等属性,通过分片渲染列表项减轻 JS+UI 线程压力。
    • 利用 getItemLayout 提前告知每行高度,避免测量造成的抖动。
  3. 减少重绘与动画优化

    • 在大范围可动画控件上尽量启用 useNativeDriver: true,将动画交给原生驱动,避免 JS 持续插针。
    • 尽量减少在 render 中进行复杂计算,可用 useMemouseCallback 缓存。
  4. 避免匿名函数 & 重复创建对象

    • 在 JSX 中避免为每次 render 创建匿名函数或对象(如 style={{...}}),否则每次都会导致子组件重新渲染。
    • 将常用回调或样式提升到外部或使用 useCallback / useMemo 优化。
  5. 使用 Hermes 性能剖析工具

    • Hermes 自带的堆栈采样工具可分析 JS 侧的性能热点。
    • 借助 systrace 工具可更精准地剖析 UI 线程与 JS 线程之间的时间分配。
  6. 慎用 InteractionManager

    • 当任务量较重时,用 InteractionManager.runAfterInteractions 推迟到交互后执行,但若任务过多仍可能造成卡顿,应酌情使用。

总结与学习建议

本文从 RN 整体架构入手,深入解读了旧架构 Bridge + Shadow Tree 以及全新的 Fabric + TurboModules 管线,结合代码示例与架构图解,帮助你快速理清 RN 底层逻辑与演进路径。要真正掌握 RN 源码,建议采取如下学习路径:

  1. 阅读官方文档与架构设计文档

    • 官方博客经常发布架构演进文章。
    • React Native 核心仓库中的 Architecture.mdTurboModules.md 等文档非常有帮助。
  2. 从简单示例入手

    • 先实现一个自定义 Native Module,再升级为 TurboModule。
    • 逐步尝试手动创建 Fabric 节点并渲染。
  3. 调试与打断点

    • 在模拟器 / 手机上运行 RN 应用时,在 Xcode 或 Android Studio 对 CatalystBridgeFabricUIManager 等处打断点,观察 JS 与 Native 之间数据流转。
  4. 关注社区与 RFC

    • React Native 社区和 React Core 团队会定期在 GitHub 上发布 RFC(Request for Comments),讨论即将到来的架构变动。
    • 关注 react-native-website 中的 “Architecture” 栏目,及时了解新特性。
  5. 实践与迁移

    • 如果你有现有项目,尝试启用 Fabric 与 TurboModules,观察代码中需要修改的地方。
    • 在不同机型与平台上测试性能差异,积累实际经验。

通过持续的源码阅读与实践,相信你很快能深入理解 React Native 的底层原理,并能够在项目中定制高性能的原生组件或优化方案。

# React Native 实战:打造音视频播放器与弹幕系统

在现代移动应用中,音视频播放已成常见需求,尤其是二次元、直播、短视频等场景,**弹幕(Danmaku)** 更能提升互动体验。本文将从零开始,带你使用 React Native 结合 `react-native-video` 和自定义弹幕组件,完整实现一个集视频播放与弹幕展示的实战示例。文章包含环境准备、关键代码、ASCII 图解与详细说明,助你快速上手并理解每一步背后的原理。

---

## 一、概述与功能需求

本示例将实现以下主要功能:

1. **视频播放器**  
   - 支持本地/网络视频播放,带基础控制(播放/暂停、进度条、倍速、全屏)。  
2. **弹幕系统**  
   - 用户点击“发送弹幕”按钮即可输入文字并发出弹幕,该弹幕会从屏幕右侧匀速飞过到左侧。  
   - 弹幕支持多行轨道、随机颜色和速度。  
3. **可扩展性**  
   - 代码结构清晰,方便后续扩展“弹幕时间轴同步”、“多视频切换”等功能。  

最终效果示意(ASCII 图解):

┌───────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 视频播放器区域 (Video) │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ 视频画面 │ │ │ │
│ │ │ │ (真实播放内容)│ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ ─────────►弹幕: “你好,世界!” │ │ │
│ │ │ ────────►弹幕: “React Native炫酷!” │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ │ 控件:播放/暂停 进度条 发送弹幕按钮 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘


---

## 二、环境准备与依赖安装

### 2.1 开发环境

- React Native ≥ 0.63  
- Node.js ≥ 12  
- Yarn 或 npm  
- Android Studio / Xcode(用于真机或模拟器测试)  

确保你已创建一个 React Native 项目,可使用以下命令:

```bash
npx react-native init RNVideoDanmakuDemo
cd RNVideoDanmakuDemo

2.2 安装关键依赖

  1. 视频播放库 react-native-video
    该库封装了 iOS/Android 原生视频组件,支持多种格式和控制。
# 使用 Yarn
yarn add react-native-video

# 或者使用 npm
# npm install react-native-video --save

在 React Native ≥ 0.60 时,Auto-linking 会自动完成原生模块集成。若遇到编译错误,可手动执行 cd ios && pod install && cd ..

  1. (可选)图标库和样式辅助
    为了让控制按钮更美观,推荐使用 react-native-vector-iconsreact-native-elements。本文示例使用原生 Text 简化演示,不强制依赖。

三、项目目录结构

为保持代码清晰,建议按功能分层。示例目录结构如下:

RNVideoDanmakuDemo/
├── android/
├── ios/
├── src/
│   ├── components/
│   │   ├── VideoPlayer.js      ← 视频播放器组件
│   │   ├── DanmakuItem.js      ← 单条弹幕组件
│   │   └── DanmakuList.js      ← 弹幕列表容器组件
│   ├── screens/
│   │   └── HomeScreen.js       ← 主页面,组合 VideoPlayer + DanmakuList
│   ├── App.js                  ← 入口,加载 HomeScreen
│   └── styles.js               ← 公共样式
└── package.json
  • VideoPlayer.js:封装 react-native-video,提供视频播放/暂停、进度条、全屏等接口。
  • DanmakuItem.js:定义单条弹幕渲染和动画。
  • DanmakuList.js:管理所有弹幕条目,负责随机轨道分配、渲染队列。
  • HomeScreen.js:主页面,包含 VideoPlayer、DanmakuList、弹幕输入控件。

四、实现视频播放器组件(VideoPlayer.js)

首先,我们实现一个基础的视频播放器,支持播放、暂停、进度监听、全屏切换等功能。

// src/components/VideoPlayer.js
import React, { useRef, useState } from 'react';
import {
  View,
  StyleSheet,
  TouchableOpacity,
  Text,
  Dimensions,
  ActivityIndicator,
} from 'react-native';
import Video from 'react-native-video';

const { width: SCREEN_WIDTH } = Dimensions.get('window');

export default function VideoPlayer({
  source,       // 视频资源:{ uri: 'http://...' } 或 require('./xxx.mp4')
  onProgress,   // 播放进度回调
  paused,       // 控制暂停/播放
  onEnd,        // 播放结束回调
}) {
  const videoRef = useRef(null);
  const [duration, setDuration] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const [isBuffering, setBuffering] = useState(false);

  // 播放器加载完成
  const onLoad = (data) => {
    setDuration(data.duration);
  };

  // 播放进度更新
  const handleProgress = (data) => {
    setCurrentTime(data.currentTime);
    onProgress && onProgress(data.currentTime, duration);
  };

  // 缓冲状态
  const onBuffer = (meta) => {
    setBuffering(meta.isBuffering);
  };

  // 播放/暂停切换示例(可由父组件控制)
  // 全屏、倍速等高级功能可基于 videoRef.current.call(video) 添加

  return (
    <View style={styles.container}>
      <Video
        ref={videoRef}
        source={source}
        style={styles.video}
        resizeMode="contain"
        paused={paused}
        onLoad={onLoad}
        onBuffer={onBuffer}
        onProgress={handleProgress}
        onEnd={onEnd}
      />
      {isBuffering && (
        <View style={styles.loadingOverlay}>
          <ActivityIndicator size="large" color="#fff" />
        </View>
      )}
      {/* 简单进度条示例 */}
      <View style={styles.progressBarContainer}>
        <View
          style={[
            styles.progressBar,
            { width: (currentTime / duration) * SCREEN_WIDTH || 0 },
          ]}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width: '100%',
    aspectRatio: 16 / 9,
    backgroundColor: '#000',
  },
  video: {
    ...StyleSheet.absoluteFillObject,
  },
  loadingOverlay: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
  },
  progressBarContainer: {
    position: 'absolute',
    bottom: 0,
    height: 4,
    width: '100%',
    backgroundColor: '#444',
  },
  progressBar: {
    height: '100%',
    backgroundColor: '#e91e63',
  },
});

4.1 关键说明

  1. <Video> 组件

    • source:可传本地 require('./sample.mp4') 或远程 { uri: 'http://...' }
    • paused:布尔值,控制播放器暂停或播放。
  2. 进度条实现

    • 通过 onLoad 拿到 duration,通过 onProgress 拿到 currentTime,计算进度宽度:(currentTime/duration) * SCREEN_WIDTH,简易展示。
  3. 缓冲指示器

    • 监听 onBuffer,若 isBuffering === true,则在视频中央显示一个 ActivityIndicator
拓展:若需支持全屏切换,可使用 react-native-orientation-locker<Video> 自带的 fullscreen 属性(但平台兼容性略有差异)。

五、实现弹幕展示组件

弹幕系统主要分为两部分:单条弹幕渲染弹幕列表管理。我们需要将新发送的弹幕动态加入队列,并让它们在屏幕上从右向左匀速飞过。

5.1 单条弹幕组件(DanmakuItem.js)

每条弹幕在屏幕上沿 X 轴从屏幕宽度到负宽度区间做动画,我们使用 Animated 实现。

// src/components/DanmakuItem.js
import React, { useEffect, useRef } from 'react';
import {
  Animated,
  Text,
  StyleSheet,
  Dimensions,
} from 'react-native';

const { width: SCREEN_WIDTH } = Dimensions.get('window');

// 轨道高度:根据行高决定
const TRACK_HEIGHT = 24;

export default function DanmakuItem({
  text,         // 弹幕文本
  color = '#fff', // 弹幕颜色,可随机
  duration = 8000, // 从屏幕右侧移到左侧所需毫秒
  trackIndex = 0,  // 轨道编号,用于垂直位置
  onComplete,   // 弹幕飞完回调
}) {
  const translateX = useRef(new Animated.Value(SCREEN_WIDTH)).current;

  useEffect(() => {
    Animated.timing(translateX, {
      toValue: -SCREEN_WIDTH, // 移动到屏幕左侧外
      duration,
      useNativeDriver: true,
    }).start(({ finished }) => {
      if (finished) {
        onComplete && onComplete();
      }
    });
  }, [translateX]);

  return (
    <Animated.View
      style={[
        styles.danmakuContainer,
        {
          top: trackIndex * TRACK_HEIGHT,
          transform: [{ translateX }],
        },
      ]}
    >
      <Text style={[styles.danmakuText, { color }]}>{text}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  danmakuContainer: {
    position: 'absolute',
    left: 0,
    // 宽度自适应文本长度,无需指定宽度
  },
  danmakuText: {
    fontSize: 16,
    fontWeight: 'bold',
    textShadowColor: '#000',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 1,
  },
});

5.1.1 关键说明

  1. translateX 动画

    • 初始值为屏幕宽度 SCREEN_WIDTH(相当于文本完全在右侧外)。
    • 调用 Animated.timing 将其线性过渡到 -SCREEN_WIDTH,使弹幕从右至左出屏。
    • duration 参数可调节弹幕速度(值越大速度越慢)。
  2. 轨道定位

    • 通过 trackIndex 决定弹幕垂直位置:top: trackIndex * TRACK_HEIGHT
    • 可根据需求设置更多行、增加间距、防止重叠。
  3. 飞完回调

    • 当动画结束后,调用 onComplete,通知弹幕管理组件将该条弹幕从队列中移除。

5.2 弹幕列表管理组件(DanmakuList.js)

管理多条弹幕,需要根据当前已有弹幕数量分配“轨道”或“行号”,避免相互遮挡。简单策略为:循环分配轨道。示例代码如下:

// src/components/DanmakuList.js
import React, { useState, useRef } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import DanmakuItem from './DanmakuItem';

const { height: SCREEN_HEIGHT } = Dimensions.get('window');

// 总共可显示的轨道数量(根据视频高度和轨道高度决定)
const MAX_TRACKS = Math.floor((SCREEN_HEIGHT * 0.56) / 24); // 假设视频区域 56% 高度

export default function DanmakuList({ danmakuData }) {
  // danmakuData: [{ id, text, color }, ...]
  // 内部维护的渲染列表
  const [renderList, setRenderList] = useState([]);
  const trackCount = useRef(0);

  // 每次 danmakuData 更新时,添加新弹幕
  React.useEffect(() => {
    if (danmakuData.length === 0) return;
    // 取出最新一条弹幕
    const latest = danmakuData[danmakuData.length - 1];
    const trackIndex = trackCount.current % MAX_TRACKS;
    trackCount.current += 1;

    const newItem = {
      ...latest,
      trackIndex,
      key: latest.id,
    };
    setRenderList((prev) => [...prev, newItem]);
  }, [danmakuData]);

  // 弹幕完成时从 renderList 中删除
  const handleComplete = (id) => {
    setRenderList((prev) => prev.filter((item) => item.id !== id));
  };

  return (
    <View style={styles.container} pointerEvents="none">
      {renderList.map((item) => (
        <DanmakuItem
          key={item.key}
          text={item.text}
          color={item.color}
          trackIndex={item.trackIndex}
          onComplete={() => handleComplete(item.id)}
        />
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    width: '100%',
    height: '100%',
  },
});

5.2.1 关键说明

  1. danmakuData 数组

    • 由父组件(HomeScreen)维护,记录所有发送过的弹幕对象,如:{ id: 123, text: 'Hello', color: '#f00' }
    • 通过 useEffect 监听 danmakuData 变化,每次新增一条时,向 renderList 中加入动态元素。
  2. 轨道分配

    • trackCount.current 用于循环分配轨道索引:trackIndex = trackCount.current % MAX_TRACKS
    • MAX_TRACKS 根据视频区域高度与单行弹幕高度决定,可根据实际需求调整。
  3. pointerEvents="none"

    • 使 DanmakuList 下的弹幕不会拦截触摸事件,确保播放器或其他控件可正常接收点击。
  4. 渲染与移除

    • renderList 中每条 item 渲染为 DanmakuItemonComplete 回调触发后执行 handleComplete,从 renderList 中删除该弹幕,停止渲染。

六、主页面整合(HomeScreen.js)

将视频播放器和弹幕系统组合在一起,并添加一个 “发送弹幕” 输入框与按钮,完成整体交互。

// src/screens/HomeScreen.js
import React, { useState, useRef } from 'react';
import {
  View,
  TextInput,
  Button,
  StyleSheet,
  Keyboard,
} from 'react-native';
import VideoPlayer from '../components/VideoPlayer';
import DanmakuList from '../components/DanmakuList';

// 生成唯一 ID
const generateId = (() => {
  let count = 0;
  return () => {
    count += 1;
    return Date.now().toString() + count;
  };
})();

export default function HomeScreen() {
  // 控制视频播放状态
  const [paused, setPaused] = useState(false);
  // 维护弹幕数据列表
  const [danmakuData, setDanmakuData] = useState([]);
  const [inputText, setInputText] = useState('');

  // 播放器进度回调示例
  const handleProgress = (currentTime, duration) => {
    // 可用于“同步弹幕时间轴”或其他逻辑
    // console.log('进度:', currentTime, '/', duration);
  };

  // 发送弹幕
  const sendDanmaku = () => {
    if (inputText.trim() === '') return;
    const newDanmaku = {
      id: generateId(),
      text: inputText.trim(),
      color: getRandomColor(),
    };
    setDanmakuData((prev) => [...prev, newDanmaku]);
    setInputText('');
    Keyboard.dismiss();
  };

  // 随机颜色生成
  const getRandomColor = () => {
    const letters = '0123456789ABCDEF';
    let color = '#';
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
  };

  return (
    <View style={styles.container}>
      {/* 视频播放器 */}
      <View style={styles.videoWrapper}>
        <VideoPlayer
          source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
          paused={paused}
          onProgress={handleProgress}
          onEnd={() => setPaused(true)}
        />
        {/* 弹幕覆盖层 */}
        <DanmakuList danmakuData={danmakuData} />
      </View>

      {/* 控制面板 */}
      <View style={styles.controls}>
        <Button
          title={paused ? '播放' : '暂停'}
          onPress={() => setPaused((prev) => !prev)}
        />
      </View>

      {/* 弹幕输入区域 */}
      <View style={styles.inputWrapper}>
        <TextInput
          style={styles.input}
          placeholder="输入弹幕..."
          value={inputText}
          onChangeText={setInputText}
        />
        <Button title="发送弹幕" onPress={sendDanmaku} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  videoWrapper: {
    width: '100%',
    aspectRatio: 16 / 9,
    backgroundColor: '#000',
  },
  controls: {
    flexDirection: 'row',
    justifyContent: 'center',
    paddingVertical: 8,
    backgroundColor: '#f5f5f5',
  },
  inputWrapper: {
    flexDirection: 'row',
    padding: 8,
    alignItems: 'center',
    borderTopWidth: 1,
    borderColor: '#ddd',
  },
  input: {
    flex: 1,
    borderColor: '#aaa',
    borderWidth: 1,
    borderRadius: 4,
    height: 40,
    paddingHorizontal: 8,
    marginRight: 8,
  },
});

6.1 关键说明

  1. 组合 VideoPlayer 与 DanmakuList

    • VideoPlayer 负责底层视频播放、进度监听、暂停/播放控制;
    • DanmakuList 通过 danmakuData 渲染所有当前浮动弹幕。
  2. 弹幕数据管理

    • danmakuData 数组存储所有待渲染弹幕,内部每条数据包含 idtextcolor
    • sendDanmaku() 新增一条弹幕,通过随机颜色和唯一 ID 保证多条弹幕不会冲突;
  3. 控制面板

    • 简单的“播放/暂停”按钮切换 paused 状态,演示如何与视频控件交互;
  4. 布局示意(ASCII 图解)

    ┌───────────────────────────────────────────────────┐
    │                   App 根容器                      │
    │  ┌─────────────────────────────────────────────┐  │
    │  │                视频与弹幕区                 │  │
    │  │  ┌───────────────────────────────────────┐  │  │
    │  │  │               <VideoPlayer>          │  │  │
    │  │  └───────────────────────────────────────┘  │  │
    │  │  ┌───────────────────────────────────────┐  │  │
    │  │  │               <DanmakuList>          │  │  │
    │  │  │ (透明覆盖层,在 VideoPlayer 之上)     │  │  │
    │  │  └───────────────────────────────────────┘  │  │
    │  └─────────────────────────────────────────────┘  │
    │  ┌─────────────────────────────────────────────┐  │
    │  │              控制面板 (播放/暂停)            │  │
    │  └─────────────────────────────────────────────┘  │
    │  ┌─────────────────────────────────────────────┐  │
    │  │           弹幕输入区 (TextInput + Button)    │  │
    │  └─────────────────────────────────────────────┘  │
    └───────────────────────────────────────────────────┘
    • VideoPlayer:展示视频内容。
    • DanmakuList:透明层,位于 VideoPlayer 之上,展示正在移动的弹幕。
    • controls:简单播放/暂停按钮。
    • inputWrapper:弹幕输入框与发送按钮,固定在底部。

七、性能与优化建议

  1. 减少弹幕重绘范围

    • 当前实现所有弹幕共用一个透明容器,若弹幕数量激增(>50 条同时飞),会对动画性能造成压力。
    • 可对弹幕按照轨道分组,分散到多个遮罩层,以减小单层重绘面积。
  2. 节流与合并

    • 如果用户短时间内发送大量弹幕,建议加入节流或限制输入频率,避免瞬时大量 DanmakuItem 同时挂载。
  3. 复用弹幕组件

    • 考虑使用对象池(Object Pooling)或类似方案,复用已经 off-screen 的 Animated.View,而非每次都创建新实例。
  4. 使用更高性能的动画驱动

    • useNativeDriver: true 已经启用原生驱动,但在复杂布局下可考虑 react-native-reanimatedreact-native-gesture-handler 优化动画。
  5. 视频缓冲与占用

    • 大文件的视频会占用大量内存和网络流量,生产环境可考虑多级缓存或使用 HLS/DASH 流式协议。
  6. 全屏模式适配

    • 若需要支持“横屏全屏播放 + 弹幕”,需处理屏幕旋转逻辑,可借助 react-native-orientation-locker 锁定横屏并调整弹幕轨道高度。

八、总结

本文完整演示了如何在 React Native 中实战搭建一个集视频播放弹幕系统于一体的应用。核心思路如下:

  1. VideoPlayer 组件

    • 使用 react-native-video 播放视频,通过 onLoadonProgress 等 API 获取播放状态,为进度条和其他交互提供数据。
  2. DanmakuItem + DanmakuList 组件

    • DanmakuItem 使用 Animated 实现从右向左匀速移动的单条弹幕;
    • DanmakuList 负责分配轨道、维护渲染队列,并在弹幕飞出屏幕后自动移除。
  3. HomeScreen 整合

    • 通过状态 danmakuData 管理弹幕列表,点击“发送弹幕”动态插入,使得新发弹幕立即生效。
    • 播放/暂停、进度监听与弹幕无缝结合,带来较为完整的用户体验。
  4. 性能与扩展

    • 通过轨道分配和 useNativeDriver 保证流畅;
    • 针对弹幕数量激增、全屏旋转等场景提出优化思路。

至此,你已经掌握了使用 React Native 打造一个音视频播放器与弹幕系统的完整流程。后续可在此基础上,继续扩展“弹幕时间轴同步”、“输入框样式美化”、“主题切换”、“弹幕分级” 等功能,让你的应用更加丰富与个性化。祝你开发顺利,动手打造出炫酷的移动视听体验!

以下内容从 React 的源码层面出发,逐步剖析其渲染与更新机制的核心原理。文章包含关键代码摘录、ASCII 图解与详细说明,力求让你在学习时能够快速抓住要点,并深入理解 React 团队是如何设计高效的更新调度与渲染流程的。


目录

  1. 前言:为什么要研究 React 渲染更新机制
  2. React 核心架构概览

    1. 组件树、虚拟 DOM 与 Fiber
    2. 调度器与任务优先级
  3. Reconciliation(协调)阶段深度解析

    1. 旧的 Stack reconciler vs 新的 Fiber reconciler
    2. Fiber 节点结构:关键字段与用途
    3. UpdateQueue 与更新队列的合并逻辑
    4. Diff 算法核心流程
  4. Commit(提交)阶段深度解析

    1. 三大子阶段:Before Mutation、Mutation、Layout
    2. 副作用列表(Effect List)的构建与执行
  5. 调度与优先级:如何保证流畅体验

    1. 协调和渲染的异步分片——work loop
    2. 优先级队列与 lane 概念
  6. Concurrent Mode(并发模式)关键改进

    1. 时间切片(Time Slicing)原理
    2. 中断与恢复机制
  7. 源码示例:追踪一次 setState 到更新的完整流程

    1. 组件调用 setState 的上报
    2. 生成更新对象并入队
    3. 调度更新并执行协调
    4. 提交阶段 DOM 更新
  8. 图解:Fiber 树与更新链路示意
  9. 常见误区与优化建议
  10. 结语:如何进一步钻研 React 源码

前言:为什么要研究 React 渲染更新机制

在日常开发中,我们使用 React 提供的高层次 API(如 useStateuseEffect、React Router 等)快速构建应用界面,却很少深入了解其底层实现。随着应用复杂度增长,性能调优与内存问题往往成为瓶颈:

  • 为什么大量元素更新时会卡顿?
  • 为什么某些场景下无法中断更新?
  • Concurrent Mode 到底改进了哪些底层流程?

了解 React 渲染与更新机制,能帮助我们:

  1. 更精准地定位性能瓶颈:知道协商(Reconciliation)与提交(Commit)的区别,可判断用 useEffect 还是 useLayoutEffect
  2. 定制高级优化策略:例如根据更新优先级区分“交互更新”(点击、动画)与“非交互更新”(数据轮询);
  3. 理解并发模式:如何无阻塞地更新界面、如何中断过期任务、如何保持界面稳定。

下面从源代码角度出发,结合代码示例与 ASCII 图解,逐步揭示 React 渲染更新机制的各个环节。


React 核心架构概览

在深入细节之前,我们先回顾 React 的整体架构。核心组件有:虚拟 DOMFiber调度器更新队列副作用(Effect)系统

组件树、虚拟 DOM 与 Fiber

  1. 组件树(Component Tree)
    React 应用由组件树组成,每个组件返回一个 React 元素(React.createElement(type, props, children)),最终构建成一棵所谓“虚拟 DOM 树”。
  2. 虚拟 DOM(Virtual DOM)
    React 会将 JSX 转译为 ReactElement 对象,如下所示:

    const element = <div className="foo"><span>你好</span></div>;
    // 等价于
    const element = React.createElement(
      'div',
      { className: 'foo' },
      React.createElement('span', null, '你好')
    );

    在更新时,React 会创建新的虚拟 DOM 树,与旧树做差异比对(diff),然后再将最小化的更新映射到真实 DOM。

  3. Fiber 架构
    为了解决大型树更新的阻塞问题,React 16 引入了 Fiber。每个虚拟节点对应一个 Fiber 节点,形成一个双向链表(childsiblingreturn 指针):

    FiberNode {
      type,            // FunctionComponent、ClassComponent、HostComponent 等
      key,
      pendingProps,    // 本次更新时的新 props
      memoizedProps,   // 上次提交时的 props
      stateNode,       // 对应的真实 DOM 节点或 Class 实例
      updateQueue,     // 对应的 setState 更新队列
      child, sibling, return, // 子节点、兄弟节点、父节点指针
      effectTag,       // 标记此次更新类型(Placement、Update、Deletion)
      nextEffect,      // 副作用链表指针
      // …… 其他字段,例如优先级 lanes、flags 等
    }

    每次触发更新时,React 都会通过 scheduleUpdateOnFiber(rootFiber) 将根 Fiber 标记为需要更新,然后进入协调(Reconciliation)与提交阶段。

调度器与任务优先级

React 并非简单地“深度优先遍历整棵树再一股脑更新”,而是通过一个调度器(Scheduler)将更新任务拆分成多个可中断的小任务(Fiber Units),并根据优先级动态安排执行。调度器中的核心概念是:

  • Sync(同步更新):优先级最高,例如 setState 在事件处理器中直接调用,新内容要马上渲染。
  • Discrete(离散事件):如点击、输入等用户交互,可打断闲置任务。
  • Continuous(连续事件):如滚动、拖拽,此类任务优先级次之。
  • Idle(空闲优先级):低优先级任务,例如日志记录、统计数据上报。

在 React 18+ 中,这一套优先级体系通过 lanes(多条优先级管道)与 Scheduler 模块协同实现,能够在单次更新中动态调整优先级、抢占当前任务、分片渲染,保证体验流畅。


Reconciliation(协调)阶段深度解析

“协调”指的是 React 将新的虚拟 DOM 树(Fiber 树)与旧的 Fiber 树对比,产生更新标记(effectTag),并构建一条副作用链表(Effect List)。这一阶段可以被中断,并在下一空闲时段恢复。

旧的 Stack reconciler vs 新的 Fiber reconciler

  • Stack reconciler(React 15 及以前)

    • 同步深度优先遍历节点,直到完成整棵树的遍历后才进行 DOM 更新。
    • 大树更新会导致主线程长时间阻塞,用户无法交互。
  • Fiber reconciler(React 16 以后)

    • 引入 Fiber 数据结构,将遍历过程拆分成“工作单元”(work unit),可以被中断、优先级抢占。
    • 每次只执行一定量的工作单元,然后让出控制权给浏览器,保证高优先级任务(如用户输入)能够及时响应。

Fiber 节点结构:关键字段与用途

下面是简化版的 Fiber 节点结构,用于说明核心字段含义:

type FiberNode = {
  // 标识
  tag: WorkTag,            // 功能标签:FunctionComponent、HostComponent、ClassComponent 等
  key: null | string,
  elementType: any,         // ReactElement.type
  type: any,                // Component Function 或 原生标签('div'、'span' 等)

  // 更新相关
  pendingProps: any,        // 本次更新的新 props
  memoizedProps: any,       // 上一次提交后的 props
  memoizedState: any,       // 上一次提交后的 state
  updateQueue: UpdateQueue, // 链表风格的 setState 更新队列

  // 树结构
  return: FiberNode | null, // 父节点
  child: FiberNode | null,  // 第一个子节点
  sibling: FiberNode | null,// 下一个兄弟节点
  index: number,            // 在父节点子链表中的索引

  // 真实节点引用
  stateNode: any,           // 对应真实 DOM 节点(HostComponent)或 Class 实例

  // 优先级与调度
  lanes: Lanes,             // 当前更新所属优先级车道
  childLanes: Lanes,        // 子树中未完成的更新优先级

  // 副作用(Effect)相关
  flags: Flags,             // 标记本 Fiber 需要做的副作用类型(Placement、Update、Deletion)
  subtreeFlags: Flags,      // 标记子树中需要收集进入副作用链表的标记
  nextEffect: FiberNode | null, // 构建的副作用链表指针
}
  1. pendingProps vs memoizedProps

    • pendingProps:当前要渲染的新属性(例如 setState 后传入的新 props)。
    • memoizedProps:上一次提交时的属性,用于和 pendingProps 做对比,决定是否需要更新。
  2. updateQueue

    • 链表风格的更新队列,存放通过 useStatesetState 等方式入队的更新对象(Update),每次协调时会将队列中所有更新依次应用到上一次的 memoizedState,计算最新 memoizedState
  3. flagssubtreeFlagsnextEffect

    • 在协调过程中,如果某个 Fiber 发生变化(插入、删除、更新属性等),会在该节点的 flags 标记相应的副作用类型(Placement、Update、Deletion)。
    • 同时,这些标记会在向上归的过程中累积到 subtreeFlags,用于告诉父 Fiber:“我的子树中有需要执行的副作用”。
    • 最终会依据 flags 构建一条链表:从根 Fiber 的 firstEffect 开始,按执行顺序串联所有需要在提交阶段执行的 Fiber,通过 nextEffect 进行遍历。

UpdateQueue 与更新队列的合并逻辑

当你在函数组件或类组件中多次调用 setStatedispatch,相应的更新并不是立刻执行,而是被收集到当前 Fiber 的更新队列(updateQueue)中。典型的 updateQueue 结构如下(以类组件为例):

type Update<State> = {
  action: any,           // setState 中传入的部分 state 或更新函数
  priority: Lanes,       // 更新优先级
  next: Update<State> | null, // 环形链表指针
}

type UpdateQueue<State> = {
  baseState: State,       // 本次更新队列应用前的 state 基础
  firstUpdate: Update<State> | null,
  lastUpdate: Update<State> | null,
  shared: {
    pending: Update<State> | null, // 待处理的更新环形链
  }
}
  1. 入队逻辑

    • 当调用 setState(updater) 时,React 会创建一个 Update 对象,将其插入到 shared.pending 环形链表末尾。
    • 如果之前已有未处理的 Update,则 newUpdate.next = firstPendingUpdate,并更新 lastPendingUpdate.next = newUpdate
  2. 消费队列

    • 在协调(beginWork)阶段,React 会从 updateQueue.shared.pending 中取出所有 Update,循环应用到 baseState

      let resultState = queue.baseState;
      let update = queue.shared.pending.next; // 第一个更新
      do {
        const action = update.action;
        resultState = typeof action === 'function' ? action(resultState, props) : { ...resultState, ...action };
        update = update.next;
      } while (update !== null && update !== queue.shared.pending.next);
    • 处理完后,将 queue.baseState 更新为新 state,将 queue.shared.pending 置空(或保留未处理更新,用于下一轮调度)。
    • memoizedState 赋值为 resultState,以供后续渲染。

Diff 算法核心流程

在 Fiber reconciler 中,主要分为两种分支:首屏渲染(mount)更新(update)

  1. 挂载(mount)阶段

    • 对每个新 Fiber(即 ReactElement 转换来的 Fiber 节点),标记 Placement,将其插入到真实 DOM(HostComponent)中。
    • 不需要比较旧节点,因为旧节点为 null,所有节点都直接视为“新节点”。
  2. 更新(update)阶段

    • 从根 Fiber 开始,深度优先遍历子树:

      • 如果 Fiber 对应的 type(组件类型)相同,执行“更新 props”逻辑,为 flags 标记 Update
      • 如果不同,则执行“删除旧节点、插入新节点”逻辑,标记 DeletionPlacement
    • 对于子节点数组,React 会进行 同级比较

      • 先处理头部与尾部的简单匹配;若都匹配不上,则构建一个键值映射(key →旧 Fiber),用来在 O(n) 时间内找到可复用节点。
      • 多余旧节点会被标记为 Deletion;多余新节点标记为 Placement

    下面是简化的 Diff 过程伪码(匹配多子节点时):

    function reconcileChildrenArray(parentFiber, oldChildren, newChildren) {
      let lastPlacedIndex = 0;
      let oldFiberMap = mapRemainingChildren(oldChildren); // key → oldFiber
    
      for (let i = 0; i < newChildren.length; i++) {
        const newChild = newChildren[i];
        const matchedOldFiber = oldFiberMap.get(newChild.key || i) || null;
    
        if (matchedOldFiber) {
          // 可复用
          const newFiber = createWorkInProgress(matchedOldFiber, newChild.props);
          newFiber.index = i;
          newFiber.return = parentFiber;
    
          // 判断是否需要移动
          if (matchedOldFiber.index < lastPlacedIndex) {
            newFiber.flags |= Placement; // 需要移动
          } else {
            lastPlacedIndex = matchedOldFiber.index;
          }
    
          placeChild(newFiber);
          oldFiberMap.delete(newChild.key || i);
        } else {
          // 新插入
          const newFiber = createFiberFromElement(newChild);
          newFiber.index = i;
          newFiber.return = parentFiber;
          newFiber.flags |= Placement;
          placeChild(newFiber);
        }
      }
    
      // 剩余 oldFiberMap 中的节点,需要删除
      oldFiberMap.forEach((fiber) => {
        fiber.flags |= Deletion;
      });
    }

    关键是:通过键值映射oldFiberMap)加速跨位置节点复用,并通过 lastPlacedIndex 标记来判断是否需要插入或移动。


Commit(提交)阶段深度解析

当协调阶段完成,Fiber 树已经标记好了各节点的 flags(Placement、Update、Deletion 等),并构建出一条按执行顺序排列的“副作用链表”(Effect List)。接下来就是Commit 阶段,分为三个子阶段依次执行:

  1. Before Mutation(突变前)

    • 在此阶段,React 会调用所有需要执行 getSnapshotBeforeUpdate 的类组件生命周期,并执行 “DOM 读取” 操作(例如测量位置)。
    • 此时 DOM 仍旧是旧版本,不能写入变更。
  2. Mutation(突变)

    • 真正对 DOM(或原生视图)进行更新:插入新节点、删除旧节点、更新属性、事件注册等。
    • 此阶段会执行所有 flags 标记为 PlacementUpdateDeletion 的 Fiber 节点对应的副作用函数(commitHook)。
  3. Layout(布局)

    • 在 DOM 发生变更后,调用所有 useLayoutEffect Hook 与 componentDidUpdate 生命周期函数,可在此时安全地读取最新布局并触发后续操作。
    • 结束后进入下一轮空闲等待。

下面用伪代码演示 Commit 阶段的高层逻辑:

function commitRoot(root) {
  const firstEffectFiber = root.current.firstEffect;
  // Before Mutation 阶段
  nextEffect = firstEffectFiber;
  while (nextEffect !== null) {
    if (nextEffect.flags & Snapshot) {
      commitBeforeMutationLifeCycles(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }

  // Mutation 阶段
  nextEffect = firstEffectFiber;
  while (nextEffect !== null) {
    const flags = nextEffect.flags;
    if (flags & Placement) {
      commitPlacement(nextEffect);
    }
    if (flags & Update) {
      commitUpdate(nextEffect);
    }
    if (flags & Deletion) {
      commitDeletion(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }

  // 将 root.current 更新为 workInProgress Fiber
  root.current = root.finishedWork;

  // Layout 阶段
  nextEffect = firstEffectFiber;
  while (nextEffect !== null) {
    if (nextEffect.flags & Layout) {
      commitLayoutEffects(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  1. commitBeforeMutationLifeCycles:调用 getSnapshotBeforeUpdateuseLayoutEffect 的布局读取逻辑。
  2. commitPlacement:将当前 Fiber 对应的 DOM 节点插入到父节点中(parentNode.insertBefore(dom, sibling))。
  3. commitUpdate:更新属性或事件绑定。
  4. commitDeletion:删除节点前先卸载子树生命周期,再从父节点中移除对应 DOM。
  5. commitLayoutEffects:执行 useLayoutEffect 回调与 componentDidUpdate 生命周期。

三阶段分离保证了:在 Mutation 阶段不做任何 DOM 读取,只关心写入;在 Layout 阶段集中处理所有影响布局的副作用,尽量减少重排(Reflow)次数。


调度与优先级:如何保证流畅体验

协调和渲染的异步分片——work loop

Fiber 核心设计之一就是能在执行协调和提交时 “中途让出”,让浏览器去处理高优先级任务(如用户点击、动画帧)。这一机制由 work loop 负责驱动:

function performUnitOfWork(fiber) {
  // 1. beginWork 阶段:对 fiber 及其子节点进行协调(diff)
  const next = beginWork(fiber, renderLanes);
  if (next !== null) {
    return next;
  }
  // 2. 如果没有子节点可协商,则向上归(completeWork)处理完当前节点
  let completed = completeUnitOfWork(fiber);
  return completed;
}

function workLoopSync() {
  while (nextUnitOfWork !== null) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
}

function workLoopConcurrent(deadline) {
  // deadline 用于判断当前帧时间是否耗尽
  while (nextUnitOfWork !== null && !deadline.timeRemaining() < threshold) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfTask);
  }
  // 如果任务尚未完成,安排下一次空闲回调
  if (nextUnitOfWork !== null) {
    scheduleCallback(workLoopConcurrent);
  } else {
    // 已完成:执行 commit 阶段
    commitRoot(root);
  }
}
  1. performUnitOfWork:执行单个 Fiber 节点的协调或归(begin/complete)逻辑,返回下一个待处理的单元。
  2. 同步模式(workLoopSync:直接循环执行所有 Fiber 单元,一鼓作气完成更新。用于优先级最高的更新(同步更新)。
  3. 并发模式(workLoopConcurrent:每次循环会检查 deadline.timeRemaining(),控制在剩余帧时间(通常 \~5ms)内尽量做更多工作,时间耗尽则“让出”给主线程,待下一空闲时间再续做剩余单元。

优先级队列与 lane 概念

在 React 18 中,调度器将多个更新分配到不同的 lane(优先级车道) 中,例如:

  • 同步车道(Sync Lane):优先级最高,立即执行,如事件处理函数中的 setState
  • 离散车道(Discrete Lane):如点击、输入、submit 等离散事件。
  • 连续车道(Continuous Lane):如滚动、动画可以被中断的任务。
  • 空闲车道(Idle Lane):低优先级,如日志上报。

每个更新会携带一个 lane 标记,进入 Scheduler 时会根据当前已有的任务与其优先级决定是否立即切换工作模式(from Sync → Concurrent → Idle),以及分片时间分配。

type Lanes = number; // 位掩码,表示一组优先级车道

function requestUpdateLane() {
  // 正在 ReactEventHandler 中:DiscreteLane
  // 正在定时器回调中:ContinuousLane
  // ...
  return selectedLane;
}

function markRootUpdated(root, lane) {
  root.pendingLanes |= lane;
  scheduleWorkOnRoot(root, lane);
}

function scheduleWorkOnRoot(root, lane) {
  const existingCallback = root.callbackNode;
  const newPriority = getHighestPriorityLane(root.pendingLanes);
  if (existingCallback !== null) {
    const existingPriority = root.callbackPriority;
    if (existingPriority === newPriority) {
      return;
    }
    // 如果优先级更高,则取消旧回调
    if (existingPriority > newPriority) {
      cancelCallback(existingCallback);
    }
  }
  // 根据 newPriority 调度相应回调:同步或并发
  if (newPriority === Sync) {
    root.callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    root.callbackPriority = Sync;
  } else {
    const schedulerPriority = laneToSchedulerPriority(newPriority);
    root.callbackNode = scheduleCallback(schedulerPriority, performConcurrentWorkOnRoot.bind(null, root));
    root.callbackPriority = newPriority;
  }
}
  1. requestUpdateLane():根据当前上下文(事件类型、是否正在 render 阶段)分配合适的 lane
  2. markRootUpdated():将更新的 lane 标记到根 Fiber 的 root.pendingLanes 中,并调用 scheduleWorkOnRoot
  3. scheduleWorkOnRoot():比较新旧优先级,决定是立即执行同步更新还是使用并发调度。

这种多车道调度策略使得:

  • 用户点击输入这类对响应时间要求高的更新,能被优先调度并同步完成;
  • 后台数据轮询、动画渐变等对时延要求不高的更新,会被分片处理,避免阻塞主线程。

Concurrent Mode(并发模式)关键改进

React 16–17 下的 Fiber 已能部分分片渲染,但在 18+ 中,**并发模式(Concurrent Mode)**进一步开放更多接口,支持更细粒度的渲染中断和恢复。

时间切片(Time Slicing)原理

并发模式会在调度更新时始终使用 workLoopConcurrent,让出控制权更频繁,避免长时间占用主线程。借助浏览器提供的 requestIdleCallback 或自定义的打包版实现,React 能在每个帧的空闲时间片内只执行一小段协调,再让出控制权,举例如下:

帧 0 开始:                    ┌─────────────┐
 主线程空闲中 → 执行 2ms 协调   │    React    │ apply Fiber units
   → 用时耗完 → 主线程被还给浏览器 │   workLoop  │    (2ms)
             ↓                  └─────────────┘
 浏览器渲染帧 0 视觉更新           ▲
   → 16ms 帧时间                  │
             ↓                  ┌─────────────┐
 主线程空闲   → 执行 2ms 协调    │    React    │ apply Fiber units
   → 用时耗完 → 主线程被还给浏览器 │   workLoop  │    (2ms)
             ↓                  └─────────────┘
 浏览器渲染帧 1

每帧只执行若干毫秒的协调,再让浏览器负责 DOM 提交与重绘,确保卡顿最小化。若在中途收到了高优先级任务(如鼠标点击事件),React 会中断当前调度,优先执行高优先级更新。

中断与恢复机制

在并发模式下,React 通过 shouldYieldToHost() 判断当前帧剩余时间,若不足则将当前 Fiber 节点(nextUnitOfWork)存储到全局状态,调用 scheduleCallback 继续后续工作。这样可以随时中断,保证用户交互优先。核心逻辑:

function performConcurrentWorkOnRoot(root, didTimeout) {
  nextUnitOfWork = createWorkInProgress(root.current, null); // 创建 workInProgress Fiber
  workLoopConcurrent(); // 执行并发工作循环
  if (nextUnitOfWork !== null) {
    // 当前帧未完成,继续调度
    root.callbackNode = scheduleCallback(performConcurrentWorkOnRoot.bind(null, root));
  } else {
    // 全部 Fiber 处理完毕,进入 commit 阶段
    commitRoot(root);
  }
}

function shouldYieldToHost() {
  // 通过 Scheduler API 判断是否已达帧时间阈值
  const timeRemaining = getCurrentTime() - frameStartTime;
  return timeRemaining >= frameDeadline; // 若超过阈值,则返回 true
}

假设 frameDeadline = 5ms,每次 performUnitOfWork 消耗 1ms,React 会在执行 5 次单元后让出控制。若遇到高优先级更新(如点击),则会在下一帧立即响应。


源码示例:追踪一次 setState 到更新的完整流程

下面以一个简化的类组件为例,手动跟踪从调用 this.setState 到 DOM 更新的整个流程:

// 简化示例:Counter.js
import React, { Component } from 'react';
import { View, Text, Button } from 'react-native';

export default class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment() {
    // ========== 1. 业务组件调用 setState ==========
    this.setState({ count: this.state.count + 1 });
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate:', prevState.count, '->', this.state.count);
  }

  render() {
    return (
      <View>
        <Text>Count: {this.state.count}</Text>
        <Button title="增加" onPress={() => this.increment()} />
      </View>
    );
  }
}

8.1 组件调用 setState 的上报

  1. React 在实例化该组件时生成一个对应的 Fiber(设为 fiber),fiber.stateNode 就是该 Counter Class 实例。
  2. 当用户点击“增加”按钮,onPress 调用 this.increment(),执行 setState

    // ReactClassComponent.js 中的 setState 调用
    public setState(partialState) {
      // this._reactInternals 就是该组件对应的 Fiber
      const fiber = this._reactInternals;
      // 1. 创建一个 Update 对象
      const update = createUpdate(SyncLane); 
      update.payload = partialState;
      // 2. 将 Update 加入到 fiber.updateQueue
      enqueueUpdate(fiber, update);
      // 3. 调度根节点更新
      scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
    }

8.2 生成更新对象并入队

  • createUpdate(lane):生成一个包含 payload={ count: oldCount+1 } 的更新对象,优先级设为 SyncLane(同步)。
  • enqueueUpdate(fiber, update):将该更新插入到 fiber.updateQueue.shared.pending 的环形链表末尾。
  • scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp):开始调度,从当前组件的 Fiber 一直向上找到根 Fiber(fiber.tag === HostRoot),标记 root.pendingLanes |= SyncLane 并调用 scheduleWorkOnRoot(root, SyncLane)

8.3 调度更新并执行协调

假设当前没有其他更高优先级任务,React 会认为这是一个同步更新,直接进入 workLoopSync

function performSyncWorkOnRoot(root) {
  // 1. 创建新的 workInProgress Fiber(双缓存:current 与 workInProgress)
  workInProgress = createWorkInProgress(root.current, null);
  // 2. 开始协调
  workLoopSync();
  // 3. 协调完成,得到新的 Fiber 树 root.finishedWork
  const finishedWork = root.finishedWork;
  // 4. 进入提交阶段,将 root.current 指向 finishedWork
  commitRoot(root);
}

8.3.1 beginWork:进入 Counter 组件

  • workLoopSync 中,调用 performUnitOfWork(nextUnitOfWork),第一次 nextUnitOfWork 是根 Fiber。最终会遍历到 Counter 组件对应的 Fiber(FunctionComponent or ClassComponent)。
  • beginWork(fiber, SyncLane):对于 ClassComponent,会执行 updateClassComponent(fiber, SyncLane, renderExpirationTime),包括以下流程:

    1. 计算新的 state:从旧的 fiber.memoizedStatefiber.updateQueue 中消费所有同步更新,得到新的 memoizedStatecount+1)。
    2. 执行 render():调用 fiber.stateNode.render() 渲染新虚拟 DOM。
    3. 构建新子 Fiber:基于 render() 返回的新 ReactElement 树,与旧的子 Fiber 树调用 reconcileChildren 生成新的子 Fiber,并标记 placement/update/deletion

8.4 提交阶段 DOM 更新

经过整个子树的协商后,React 得到一条副作用链(Effect List),记录了“哪些节点需要插入、删除、更新”。此时执行 commitRoot(root)

  1. Before Mutation:调用 getSnapshotBeforeUpdate(若有)。
  2. Mutation

    • 对 Counter 组件对应的 DOM 节点(Text)进行更新,因为 memoizedPropspendingProps 不同,标记 Update,在 commitUpdate(fiber) 中执行 textNode.textContent = newText
    • 如果 Counter 有新增子节点或子节点删除,也会在此阶段同步到真实 DOM。
  3. Layout:调用 componentDidUpdate(Counter 中的日志输出)。
  4. 最终将 root.current = root.finishedWork,完成一次更新。

流程结束后,页面上会立刻看到 Count: 1,且在控制台打印 componentDidUpdate: 0 -> 1


图解:Fiber 树与更新链路示意

下面用 ASCII 图展示一个简化的场景:初始渲染与一次 setState 更新的 Fiber 树演变过程。

9.1 初始渲染时的 Fiber 树

假设 App 渲染结构如下:

<App>
  <Counter />
</App>

对应的 Fiber 树(简化版):

HostRootFiber
└─ Fiber(AppComponent)
   └─ Fiber(CounterComponent)
      └─ Fiber(ViewHost)        // RN 原生 View
         └─ Fiber(TextHost)      // 显示 “Count: 0”
         └─ Fiber(ButtonHost)
  • 每个 Fiber 除了 typeprops 外,memoizedState 初始为 { count: 0 }(Counter 组件的 state)。
  • 所有 flags 均为 0,因为是首次挂载,实际会在 Mount 阶段给对应宿主节点打上 Placement 标记。

9.2 调用 setState 后的更新流

  1. 在 Counter 组件实例里调用 setState({ count: 1 }),生成一个 Update,插入 CounterFiber 的 UpdateQueue。
  2. 调度到根 Fiber,进入同步工作循环。
  3. 在 CounterFiber 的 beginWork 阶段,React 会从 UpdateQueue 中消费更新:

    • memoizedState = { count: 0 }
    • 应用 update 的 payload { count: 1 } → 得到新 memoizedState = { count: 1 }
  4. CounterFiber 执行 render(),返回新的子树(新的 <View><Text>Count: 1</Text>...</View>)。React 将对比新旧子树,发现 <Text> 的文本内容变了,标记其 Fiber 的 flags = Update
  5. 归的过程中,Fiber 树的 flags 分别累积到父节点的 subtreeFlags
  6. 完成协调后,进入提交阶段:找到标记了 Update 的 TextFiber,执行 commitUpdatetextNode.textContent = "Count: 1"

更新后 Fiber 树的核心字段变化如下(只展示 Counter 相关):

Fiber(CounterComponent)
├─ memoizedState: { count: 1 }
├─ child: Fiber(ViewHost)
│   └─ child: Fiber(TextHost)
│       ├─ memoizedProps: { children: "Count: 1" }
│       ├─ flags: Update
│   └─ sibling: Fiber(ButtonHost)  // Button 不需要更新,flags: 0

最终真实 DOM 更新完成,用户界面从 “Count: 0” 更新到 “Count: 1”。


常见误区与优化建议

  1. 误区:所有 setState 都会同步执行

    • 其实 React 18+ 在事件回调中触发的 setState 是同步优先级(Sync Lane),会立即执行更新。但在异步回调(例如 setTimeout、XHR 回调)中触发的 setState 会被分配到不同优先级,可以并发执行。
  2. 误区:Updating setState 会立即更新 DOM

    • React 在同步更新时确实会尽快进行协调与提交,但在并发模式(startTransition)下,更新可能被延后,以免阻塞更高优先级的渲染。
  3. 优先使用 useStateuseReducer 而非手动修改 Context

    • 对于共享状态,如果仅用 Context 而不配合 useReducer,每次修改都会导致全部订阅组件重渲染,性能难以控制。
  4. 避免在 render 中做大计算

    • render 阶段是可中断的,但过于耗时的计算会增加各个 Fiber 的执行时间,导致中断点变少,影响并发体验。可将耗时逻辑放到 useMemouseEffect 或后端处理。
  5. 合理拆分组件

    • 过大的组件会导致单一 Fiber 包含大量子孙节点,更新时一次性需遍历的节点过多,不利于中断调度。可考虑拆分成更小的子组件。
  6. 避免在 useLayoutEffect 中做大量 DOM 操作

    • useLayoutEffect 会在 Mutation 阶段后立即执行,有可能导致布局抖动,影响渲染流畅。仅在必要时使用。

结语:如何进一步钻研 React 源码

本文详细剖析了 React 渲染更新机制的各个关键环节:

  • Fiber 数据结构与协调(Reconciliation)
  • Commit 阶段的三次子阶段划分
  • 调度器、多优先级 lanes 与并发时间切片
  • 整个更新流程的代码示例与图解追踪

要进一步深入,可以从以下几个方向继续探索:

  1. 深入 Scheduler 调度器

    • 阅读 scheduler 源码,理解 requestIdleCallbackshouldYieldToHost、四种优先级如何转换到 browser callback。
  2. 研究 Hooks 源码

    • useStateuseEffectuseReducer 在 Fiber 内部是如何注册与销毁的,关注 mountHookupdateHook 等实现细节。
  3. 并发特性

    • 在 React 18+ 中启用 createRoot 并体验并发模式,阅读 ReactFiberConcurrentUpdates.jsReactFiberWorkLoop.js 等文件,体会新增的 startTransitionuseDeferredValue 等 Hook 如何与调度器协作。
  4. 内存泄漏与回收

    • 了解 React 如何回收被删节点的 Fiber,如 completeDeletionclearContainer 的实现,以及与 JS 垃圾回收的关系。
  5. 源码调试技巧

    • [...]/packages/react-reconciler/src/ReactFiberWorkLoop.new.js 等文件设置断点,结合 DevTools 观察 Fiber 节点状态变化。

希望本文能帮助你搭建学习 React 源码的“第一座桥”,并为性能优化与调度改进提供有力支撑。继续深入研究,吸收更多底层原理,你将能更加自如地运用 React,创造出既易维护又高性能的前端应用。

React Native与Android原生Activity页面跳转全攻略

在移动开发领域,React Native(以下简称“RN”)凭借“一套代码,多端运行”的优势迅速流行。但在实际项目中,我们常常需要与 Android 原生模块打通,例如:从 RN 界面直接跳转到某个原生 Activity,或者在原生 Activity 中返回结果后继续在 RN 中处理。本文将围绕以下几个核心场景展开详解,并提供完整代码示例、ASCII 图解与详细说明,帮助你轻松理解并快速上手:

  1. RN 调用原生 Activity(无参/有参)
  2. 原生 Activity 返回结果给 RN
  3. 原生侧启动 RN 界面(Deep Linking 与 Intent)
  4. React Navigation 与原生跳转的结合示例

一、环境与项目准备

  1. React Native 环境

    • 本文示例基于 React Native 0.65+,Node.js v14+,Android Studio 4.0+。
    • 假设项目已经通过 npx react-native init MyApp 成功创建,并能正常运行:

      cd MyApp
      npx react-native run-android
  2. Android 原生环境

    • 使用 Android Studio 打开 MyApp/android 目录。
    • 确保 minSdkVersion ≥ 21,编译 SDK 版本与目标 SDK 版本均为 30 及以上。
  3. 目录结构示例

    MyApp/
    ├── android/              ← Android 原生工程
    │   ├── app/
    │   │   ├── src/
    │   │   │   ├── main/
    │   │   │   │   ├── java/com/myapp/         ← Java 源码
    │   │   │   │   │   ├── MainActivity.java
    │   │   │   │   │   ├── MyApp.java
    │   │   │   │   │   └── MyModule.java        ← 我们将自定义 NativeModule
    │   │   │   │   ├── AndroidManifest.xml
    │   │   │   │   └── res/...
    │   └── build.gradle
    ├── ios/                  ← iOS 工程(本文不涉及)
    ├── index.js
    ├── App.js
    └── package.json
  4. AndroidManifest.xml

    • <application> 节点中,RN 默认的 MainActivity 会包含 <intent-filter>,用于处理 Deep Linking 或启动入口。后续如果我们新建原生页面 SecondActivity,也需要在这里注册。
    <!-- android/app/src/main/AndroidManifest.xml -->
    <application
        android:name=".MainApplication"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:allowBackup="false"
        android:theme="@style/AppTheme">
    
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
    
            <!-- Deep Linking 示例方式 -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="myapp" android:host="open" />
            </intent-filter>
        </activity>
    
        <!-- 1. 新增: 原生 SecondActivity 注册 -->
        <activity
            android:name=".SecondActivity"
            android:label="Second Page"
            android:exported="true">
        </activity>
    
    </application>
    • 以上我们在 MainActivity 中配置了 Deep Linking(myapp://open 可唤起 RN 界面)。
    • 同时新建原生 SecondActivity,未来可在 RN 中通过 NativeModule 直接启动它。

二、场景一:React Native 调用原生 Activity(无参/有参)

RN 与原生的交互通常通过 NativeModule 进行桥接。我们需要在 Android 端实现一个自定义 Module,用来封装 startActivity() 的逻辑,再在 RN 端通过 NativeModules 调用。

2.1 在 Android 原生端:创建 MyModule.java

  1. 新建 Java 文件
    android/app/src/main/java/com/myapp/ 下新建 MyModule.java

    // android/app/src/main/java/com/myapp/MyModule.java
    package com.myapp;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.util.Log;
    
    import androidx.annotation.NonNull;
    
    import com.facebook.react.bridge.ActivityEventListener;
    import com.facebook.react.bridge.Arguments;
    import com.facebook.react.bridge.Callback;
    import com.facebook.react.bridge.Promise;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.bridge.ReactContextBaseJavaModule;
    import com.facebook.react.bridge.ReactMethod;
    import com.facebook.react.bridge.WritableMap;
    
    public class MyModule extends ReactContextBaseJavaModule implements ActivityEventListener {
    
        private static final String TAG = "MyModule";
        private static final int REQUEST_CODE = 1234;
        private Promise mPendingPromise;
    
        public MyModule(@NonNull ReactApplicationContext reactContext) {
            super(reactContext);
            reactContext.addActivityEventListener(this);
        }
    
        @NonNull
        @Override
        public String getName() {
            return "MyModule"; // RN 端通过 NativeModules.MyModule 访问
        }
    
        /**
         * 2.1.1 无参启动 SecondActivity
         */
        @ReactMethod
        public void startSecondActivity() {
            Activity currentActivity = getCurrentActivity();
            if (currentActivity == null) {
                Log.e(TAG, "Activity 为空,无法跳转");
                return;
            }
            Intent intent = new Intent(currentActivity, SecondActivity.class);
            currentActivity.startActivity(intent);
        }
    
        /**
         * 2.1.2 带参数启动,并希望获取返回结果 (startActivityForResult)
         * @param message 要传递的字符串参数
         * @param promise  用于回调给 JS 端结果
         */
        @ReactMethod
        public void startSecondActivityForResult(String message, Promise promise) {
            Activity currentActivity = getCurrentActivity();
            if (currentActivity == null) {
                promise.reject("ACTIVITY_NULL", "Activity 为空,无法跳转");
                return;
            }
            mPendingPromise = promise; // 保存 promise,待 onActivityResult 回调时使用
    
            Intent intent = new Intent(currentActivity, SecondActivity.class);
            intent.putExtra("message", message);
            currentActivity.startActivityForResult(intent, REQUEST_CODE);
        }
    
        // 2.1.3 onActivityResult 回调处理
        @Override
        public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
            if (requestCode == REQUEST_CODE) {
                if (mPendingPromise == null) return;
                if (resultCode == Activity.RESULT_OK) {
                    String result = data.getStringExtra("result");
                    WritableMap map = Arguments.createMap();
                    map.putString("result", result);
                    mPendingPromise.resolve(map);
                } else {
                    mPendingPromise.reject("RESULT_ERROR", "SecondActivity 返回失败");
                }
                mPendingPromise = null;
            }
        }
    
        @Override
        public void onNewIntent(Intent intent) {
            // 不需要处理
        }
    }
    • getName():返回给 JS 侧使用的模块名,此处命名为 "MyModule"
    • startSecondActivity():无参启动。
    • startSecondActivityForResult(String, Promise):带一个 message 参数并期望在 SecondActivity 结束时通过 Promise 将结果回调给 JS。
    • onActivityResult(...):当原生 Activity 返回结果时,使用 mPendingPromise.resolve(...)reject(...) 将结果传回 JS。
  2. 新建 SecondActivity.java
    在同一目录下新建 SecondActivity.java,用于测试跳转与返回。

    // android/app/src/main/java/com/myapp/SecondActivity.java
    package com.myapp;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    import androidx.annotation.Nullable;
    
    public class SecondActivity extends Activity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second); // 我们示例将新建对应布局
    
            TextView tvMessage = findViewById(R.id.tv_message);
            Button btnReturn = findViewById(R.id.btn_return);
    
            // 获取从 RN 传过来的 message
            String message = getIntent().getStringExtra("message");
            if (message != null) {
                tvMessage.setText("来自RN的参数: " + message);
            } else {
                tvMessage.setText("Hello from SecondActivity");
            }
    
            // 点击按钮后返回结果给 RN
            btnReturn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent resultIntent = new Intent();
                    resultIntent.putExtra("result", "原生Activity返回的数据");
                    setResult(RESULT_OK, resultIntent);
                    finish();
                }
            });
        }
    }
    • setContentView(R.layout.activity_second):需要在 android/app/src/main/res/layout/ 下新建一个 activity_second.xml 布局。
  3. 创建 activity_second.xml 布局

    <!-- android/app/src/main/res/layout/activity_second.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        android:gravity="center">
    
        <TextView
            android:id="@+id/tv_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello from SecondActivity"
            android:textSize="18sp" />
    
        <Button
            android:id="@+id/btn_return"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="点击返回给RN" />
    </LinearLayout>
  4. 注册 MyModule 到 React Native
    MainApplication.java 中,将 MyModule 添加到包列表。

    // android/app/src/main/java/com/myapp/MainApplication.java
    package com.myapp;
    
    import android.app.Application;
    import com.facebook.react.PackageList;
    import com.facebook.react.ReactApplication;
    import com.facebook.react.ReactNativeHost;
    import com.facebook.react.ReactPackage;
    import com.facebook.react.shell.MainReactPackage;
    import com.facebook.soloader.SoLoader;
    import java.util.List;
    
    public class MainApplication extends Application implements ReactApplication {
    
        private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
            @Override
            public boolean getUseDeveloperSupport() {
                return BuildConfig.DEBUG;
            }
    
            @Override
            protected List<ReactPackage> getPackages() {
                List<ReactPackage> packages = new PackageList(this).getPackages();
                // 1. 手动添加 MyPackage
                packages.add(new MyPackage());
                return packages;
            }
    
            @Override
            protected String getJSMainModuleName() {
                return "index";
            }
        };
    
        @Override
        public ReactNativeHost getReactNativeHost() {
            return mReactNativeHost;
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            SoLoader.init(this, /* native exopackage */ false);
        }
    }
  5. 创建 MyPackage.java

    // android/app/src/main/java/com/myapp/MyPackage.java
    package com.myapp;
    
    import com.facebook.react.ReactPackage;
    import com.facebook.react.bridge.NativeModule;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.uimanager.ViewManager;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class MyPackage implements ReactPackage {
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> modules = new ArrayList<>();
            modules.add(new MyModule(reactContext));
            return modules;
        }
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
    • MyPackageMyModule 注册进 RN 桥接。
  6. Gradle 同步并编译

    cd android
    ./gradlew clean
    ./gradlew assembleDebug
    cd ..
    • 确保编译无误,再运行 npx react-native run-android,以验证原生修改未出错。

2.2 在 React Native 端:调用 MyModule

  1. 引入 NativeModules
    在 RN 端(例如 App.js)引入并调用:

    // App.js
    import React, { useState } from 'react';
    import { View, Text, Button, StyleSheet, NativeModules } from 'react-native';
    
    const { MyModule } = NativeModules;
    
    export default function App() {
      const [result, setResult] = useState(null);
    
      // 无参跳转
      const goToSecond = () => {
        MyModule.startSecondActivity();
      };
    
      // 带参跳转并接收返回
      const goToSecondForResult = async () => {
        try {
          const res = await MyModule.startSecondActivityForResult('你好,原生!');
          setResult(res.result);
        } catch (e) {
          console.error(e);
        }
      };
    
      return (
        <View style={styles.container}>
          <Text style={styles.title}>React Native 与原生 Activity 跳转示例</Text>
          <Button title="无参跳转到 SecondActivity" onPress={goToSecond} />
          <View style={styles.spacer} />
          <Button
            title="带参跳转并返回结果"
            onPress={goToSecondForResult}
          />
          {result && (
            <>
              <View style={styles.spacer} />
              <Text style={styles.resultText}>返回结果:{result}</Text>
            </>
          )}
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
      title: { fontSize: 20, fontWeight: 'bold', marginBottom: 24, textAlign: 'center' },
      spacer: { height: 16 },
      resultText: { marginTop: 16, fontSize: 16, color: 'green' },
    });
    • 点击第一个按钮,RN 调用 MyModule.startSecondActivity(),直接打开 SecondActivity
    • 点击第二个按钮,RN 调用 MyModule.startSecondActivityForResult('你好,原生!'),并等待返回结果;
    • SecondActivity 中拿到参数后在界面显示,点击“返回给RN”按钮,将结果通过 Intent 携带并回传,RN 端 await 后显示在页面上。

    示例对话图解:

    [RN App] --(startSecondActivityForResult)--> [SecondActivity]
         |                                           |
         |<-------- onActivityResult(result)---------|
         |
     setResult("原生Activity返回的数据")

三、场景二:原生 Activity 启动 React Native 界面(Deep Linking 与 Intent)

除了 RN 调用原生页面,有时需要在原生侧直接跳转到 RN 界面。例如:在某个业务模块里,点击“编辑”按钮需要从原生 Activity 跳回 RN 页面并携带参数。常见做法包括:

  1. Deep Linking(使用 URI 方式指向 RN 页面)
  2. Explicit Intent + 标记跳转参数

下面分别介绍这两种方式。

3.1 Deep Linking(URI Scheme)

Deep Linking 通过 URL Scheme 或 App Link 唤起应用,并由 RN 的 Linking 模块监听到对应路径,从而导航到 JS 路由中指定页面。

  1. AndroidManifest.xml 中配置 Deep Link
    我们在 MainActivity<intent-filter> 中已添加如下内容:

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="open" android:pathPrefix="/profile" />
    </intent-filter>
    • 当其他原生页面或外部应用执行:

      Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("myapp://open/profile?userId=42"));
      startActivity(intent);

      就会触发 RN 的主 Activity 启动并携带该 URI。

  2. 在 RN 端监听 Linking
    App.js 或最顶部的组件中,使用 Linking 模块监听 url 事件,并根据路径进行导航(例如使用 React Navigation)。

    // App.js
    import React, { useEffect } from 'react';
    import { View, Text, Linking, Alert } from 'react-native';
    import { NavigationContainer } from '@react-navigation/native';
    import { createStackNavigator } from '@react-navigation/stack';
    
    function HomeScreen({ navigation }) {
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
          <Text>Home Screen</Text>
        </View>
      );
    }
    
    function ProfileScreen({ route }) {
      const { userId } = route.params || {};
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
          <Text>Profile Screen for userId: {userId}</Text>
        </View>
      );
    }
    
    const Stack = createStackNavigator();
    
    export default function App() {
      useEffect(() => {
        // 1. 获取应用初始启动时可能携带的 URL
        Linking.getInitialURL().then((url) => {
          if (url) {
            handleDeepLink({ url });
          }
        });
    
        // 2. 监听在应用已启动时的新 URL
        const subscription = Linking.addEventListener('url', handleDeepLink);
        return () => subscription.remove();
      }, []);
    
      const handleDeepLink = ({ url }) => {
        // 解析 URL,例如: myapp://open/profile?userId=42
        const parsed = Linking.parse(url);
        if (parsed.path === 'open/profile') {
          const userId = parsed.queryParams.userId;
          // 通过 navigation 导航到 ProfileScreen,传递参数
          navigationRef.current?.navigate('Profile', { userId });
        } else {
          Alert.alert('未知 Deep Link', url);
        }
      };
    
      // 需定义 navigationRef 用于从外部调用 navigation
      const navigationRef = React.useRef();
    
      return (
        <NavigationContainer ref={navigationRef}>
          <Stack.Navigator initialRouteName="Home">
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Profile" component={ProfileScreen} />
          </Stack.Navigator>
        </NavigationContainer>
      );
    }
    • Linking.getInitialURL():当应用冷启动时,如果包含 Deep Link,会在这里拿到 URL。
    • Linking.addEventListener('url', callback):当应用后台时再次唤起带有 Deep Link 的 URL,会触发此监听器。
    • Linking.parse(url):RN 内置解析函数,将 URL 拆分为 { scheme, hostname, path, queryParams }
    • 由此我们可以根据 path === 'open/profile' 导航到 ProfileScreen,并传递 userId 参数。

    Deep Linking 图解:

    ┌──────────────────────────┐
    │ 原生/第三方 App 或 网页  │
    │  Intent(url=myapp://open/profile?userId=42)  │
    └──────────────────────────┘
               │
               ▼
    ┌─────────────────────────────────────────┐
    │ Android 系统根据 <intent-filter> 匹配  │
    │ → 唤起 RN MainActivity 并携带该 URL     │
    └─────────────────────────────────────────┘
               │
               ▼
    ┌─────────────────────────────────────────┐
    │ React Native: Linking.getInitialURL()   │
    │ → 获取 URL 并分发到 handleDeepLink     │
    └─────────────────────────────────────────┘
               │
               ▼
    ┌─────────────────────────────────────────┐
    │ RN NavigationContainer: navigate('Profile', { userId: 42 }) │
    └─────────────────────────────────────────┘
               │
               ▼
    ┌─────────────────────────────────────────┐
    │ 加载 ProfileScreen 并显示 userId 信息  │
    └─────────────────────────────────────────┘

3.2 Explicit Intent(原生直接跳转 RN 页面)

有时不想依赖 URL Scheme,希望在原生 Activity 中直接启动 RN 页面并携带参数。我们可以在原生侧构造一个 Intent 启动 MainActivity(RN 主 Activity),并在 Intent 中放置额外参数。然后在 RN 端通过 getInitialURL()Linking.getInitialURL() 获取这些参数。

  1. 在原生侧启动 MainActivity
    例如在另一个原生 Activity(如 NativeTriggerActivity)中:

    // android/app/src/main/java/com/myapp/NativeTriggerActivity.java
    package com.myapp;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    
    public class NativeTriggerActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_native_trigger);
    
            Button btnOpenRN = findViewById(R.id.btn_open_rn);
            btnOpenRN.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(NativeTriggerActivity.this, MainActivity.class);
                    // 给 RN 传递参数:
                    intent.putExtra("screen", "Profile");
                    intent.putExtra("userId", "99");
                    startActivity(intent);
                    finish();
                }
            });
        }
    }
    • MainActivity(ReactActivity)默认会加载 index.js,并且如果 Intent 中带有额外键值对,它们会注入到 RN 侧的启动 URL 或 initialProps 中。
  2. MainActivity.java 处理 Intent 并传递给 JS
    MainActivity 通常继承自 ReactActivity,我们可以重写 getLaunchOptions() 方法,将 Intent 中的参数传递给 JS 端:

    // android/app/src/main/java/com/myapp/MainActivity.java
    package com.myapp;
    
    import android.content.Intent;
    import android.os.Bundle;
    
    import com.facebook.react.ReactActivity;
    import com.facebook.react.ReactActivityDelegate;
    import com.facebook.react.ReactRootView;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class MainActivity extends ReactActivity {
    
        @Override
        protected String getMainComponentName() {
            return "MyApp";
        }
    
        // 重写:向 JS 传递 initialProps
        @Override
        protected Bundle getLaunchOptions() {
            Intent intent = getIntent();
            Bundle initialProps = new Bundle();
    
            if (intent != null && intent.hasExtra("screen")) {
                initialProps.putString("screen", intent.getStringExtra("screen"));
            }
            if (intent != null && intent.hasExtra("userId")) {
                initialProps.putString("userId", intent.getStringExtra("userId"));
            }
            return initialProps;
        }
    }
  3. 在 RN 端读取 initialProps 并导航
    index.jsApp.js 中,通过 AppRegistry.registerComponent 时,RN 会自动将 initialProps 传入根组件 App。我们可在 App.js 中读取这些 props 并启动导航。

    // App.js
    import React, { useEffect } from 'react';
    import { View, Text, Platform } from 'react-native';
    import { NavigationContainer } from '@react-navigation/native';
    import { createStackNavigator } from '@react-navigation/stack';
    
    function HomeScreen() {
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
          <Text>Home Screen</Text>
        </View>
      );
    }
    
    function ProfileScreen({ route }) {
      const { userId } = route.params || {};
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
          <Text>Profile for userId: {userId}</Text>
        </View>
      );
    }
    
    const Stack = createStackNavigator();
    
    export default function App(props) {
      const { screen, userId } = props; // 从 native getLaunchOptions 传入
    
      return (
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Home">
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Profile" component={ProfileScreen} />
          </Stack.Navigator>
    
          {/* 一旦组件挂载,就检查初始参数,进行导航 */}
          {screen === 'Profile' && Platform.OS === 'android' && (
            // 注意:需要访问 navigationRef 才能导航,在此简化示例
            <RedirectToProfile userId={userId} />
          )}
        </NavigationContainer>
      );
    }
    
    // RedirectToProfile.js(简化示例,实际需使用 navigationRef)
    import { useEffect } from 'react';
    import { useNavigation } from '@react-navigation/native';
    
    export function RedirectToProfile({ userId }) {
      const navigation = useNavigation();
      useEffect(() => {
        if (userId) {
          navigation.navigate('Profile', { userId });
        }
      }, [userId]);
      return null;
    }
    • MainActivity.getLaunchOptions() 会将原生传进来的 screenuserId 作为 initialProps 传给 RN 根组件。
    • 在 RN 渲染时,props.screen === 'Profile',通过一个类似 RedirectToProfile 的逻辑立即导航到 ProfileScreen,并传递 userId

    显式 Intent 跳转图解:

    ┌─────────────────────────────┐
    │ NativeTriggerActivity 点击 │
    │ Intent(MainActivity, extras.{screen:Profile,userId:99}) │
    └─────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────┐
    │ MainActivity 加载 RN Bundle │
    │ getLaunchOptions 读取 extras │
    │ → initialProps = {screen, userId} │
    └─────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────┐
    │ RN App.js 收到 props.screen=Profile │
    │ 立即 navigate('Profile',{userId})    │
    └─────────────────────────────┘
                 │
                 ▼
    ┌─────────────────────────────┐
    │ ProfileScreen 显示 userId=99 │
    └─────────────────────────────┘

四、场景三:React Navigation 与原生跳转结合示例

大多数 RN 项目都会使用 React Navigation 管理界面跳转。在上述 Deep Linking 与 Explicit Intent 中,我们对 React Navigation 的集成方式略显简化。接下来给出一个更完整的、在 App 启动时即检查 initialProps 并导航到指定页面的示例。

4.1 安装 React Navigation

yarn add @react-navigation/native @react-navigation/stack
yarn add react-native-screens react-native-safe-area-context

在 Android 端,确保在 MainActivity.java 中添加 ReactActivityDelegate 配置以启用 react-native-screens

// android/app/src/main/java/com/myapp/MainActivity.java
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.rnscreens.RNScreensPackage; // 引入 react-native-screens

public class MainActivity extends ReactActivity {

    @Override
    protected String getMainComponentName() {
        return "MyApp";
    }

    @Override
    protected ReactActivityDelegate createReactActivityDelegate() {
        return new ReactActivityDelegate(this, getMainComponentName()) {
            @Override
            protected ReactRootView createRootView() {
                // 启用 react-native-screens
                return new ReactRootView(MainActivity.this);
            }
        };
    }

    @Override
    protected Bundle getLaunchOptions() {
        Intent intent = getIntent();
        Bundle initialProps = new Bundle();
        if (intent != null && intent.hasExtra("screen")) {
            initialProps.putString("screen", intent.getStringExtra("screen"));
        }
        if (intent != null && intent.hasExtra("userId")) {
            initialProps.putString("userId", intent.getStringExtra("userId"));
        }
        return initialProps;
    }
}

4.2 App.js 完整示例

// App.js
import React, { useEffect, useRef, useState } from 'react';
import { View, Text, Button, Platform } from 'react-native';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

function ProfileScreen({ route }) {
  const { userId } = route.params || {};
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile Screen for userId: {userId}</Text>
    </View>
  );
}

const Stack = createStackNavigator();

export default function App(props) {
  const navigationRef = useNavigationContainerRef();
  const [initialRoute, setInitialRoute] = useState('Home');
  const [initialParams, setInitialParams] = useState({});

  useEffect(() => {
    // 1. 读取 initialProps
    const { screen, userId } = props;
    if (screen === 'Profile' && userId) {
      setInitialRoute('Profile');
      setInitialParams({ userId });
    }
  }, [props]);

  // 2. 使用 React Navigation 设置 initialState
  const linking = {
    prefixes: [], // 不再使用 Deep Linking 示例
    config: {
      initialRouteName: initialRoute,
      screens: {
        Home: 'home',
        Profile: 'profile/:userId',
      },
    },
  };

  return (
    <NavigationContainer ref={navigationRef} linking={linking}>
      <Stack.Navigator initialRouteName={initialRoute}>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen
          name="Profile"
          component={ProfileScreen}
          initialParams={initialParams} // 这里传入 initialParams
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
  • initialRouteinitialParams 来自于原生 getLaunchOptions() 中传递的 props。
  • Stack.Navigator 中,将 initialRouteName={initialRoute},并对 Profile 屏幕设置 initialParams={initialParams}

这样在启动时,若 props.screen === 'Profile',RN 会直接打开 ProfileScreen 并显示 userId,否则进入默认的 HomeScreen


五、常见问题与性能优化

5.1 Activity 频繁重启

  • 如果 RN 端多次调用 startActivity()startActivityForResult(),可能会造成多个 SecondActivity 重叠或频繁打开。
  • 建议在跳转前先检查当前是否已有该 Activity 在栈顶,必要时可设置 launchMode="singleTop" 或在 Intent 中添加 Intent.FLAG_ACTIVITY_SINGLE_TOP 等 flag。

5.2 回退栈管理

  • RN 与原生 Activity 共存时,Back 按钮的行为需要特别注意:

    • SecondActivity 中按 “返回” 会自动调用 finish() 回到 RN。
    • 如果在 RN 页面中集成了硬件返回键监听(BackHandler),需要先判断是否要拦截返回事件,否则可能会同时触发 RN 和原生的 Back 逻辑,导致页面回退异常。

5.3 性能与内存

  • 每次启动 Activity 都会涉及 RN JS Bundle 再次初始化,可能会有短暂的白屏或性能抖动。
  • 可以使用 Single Activity + Fragment 方案(将原生页面做成 Fragment),然后在同一个 Activity 内切换 Fragment,与 RN 协作更顺畅。
  • 对于简单交互,考虑使用 React Native Navigation 这类原生导航库,直接将 RN 界面作为原生 ActivityFragment,以获得更好的性能与过渡动画。

六、小结与学习路径

本文从最基础的 RN ↔ 原生 Activity 跳转案例入手,详细介绍了以下核心内容:

  1. RN 调用原生 Activity

    • 无参跳转:getCurrentActivity().startActivity(intent)
    • 有参跳转并返回结果:startActivityForResult(intent, requestCode) + Promise 回调
  2. 原生 Activity 调用 RN 界面

    • Deep Linking:通过 LINKING<intent-filter> 实现 URL Scheme 唤起 RN 并导航
    • Explicit Intent:在原生侧构造 Intent 并通过 getLaunchOptions() 将参数传给 RN。
  3. React Navigation 与原生跳转结合

    • App.js 中根据 initialProps 决定初始路由与参数,结合 navigationRef 实现“原生 → RN”导航。
  4. 常见问题与优化

    • Activity 重叠、回退冲突、性能优化等实战建议。

通过本文示例,你应能够完成以下几项关键能力:

  • 在 RN 代码中通过 NativeModules 调用 Android 原生页面,并在原生页面中返回结果给 JS。
  • 在 Android 原生侧通过 Intent/Deep Linking 唤起 RN 页面,并将参数传递给 JS 层。
  • 在 RN 端结合 React Navigation,根据不同启动参数控制首屏路由和参数传递。
  • 解决混合导航场景下的回退、栈管理与性能问题。

下一步推荐学习内容:

  1. React Native Navigation(Wix、React Navigation)更深入的自定义动画与原生交互。
  2. 单 Activity + Fragment 架构:将原生页面封装为 Fragment,与 RN 同在一个 Activity 中管理。
  3. 跨平台 Deep Linking:在 iOS 与 Android 上同时配置 Deep Linking、Universal Links 与 App Links,打造统一路径方案。
  4. React Native 原生 UI 组件开发:学习如何自定义原生 ViewFragment,并在 RN 中使用 requireNativeComponent 引用。

希望本文能帮你构建 React Native 与 Android 原生无缝衔接的页面导航体系,让你快速在项目中实现混合导航、原生跳转与 RN 交互。

# React Native携手ArcGIS:SketchEditorCtrl地图开发实战

在移动端开发中,地图交互已成为很多应用的核心功能之一。例如:地块标绘、路径规划、地理信息采集等。本文将通过“React Native + ArcGIS Runtime SDK for Android/iOS”结合 `SketchEditorCtrl`(Sketch Editor 控制器)示例,深入演示如何在 React Native 中集成 ArcGIS 地图,并实现在线标绘、编辑要素等实战功能。文章内容包含环境准备、安装配置、核心代码示例、图解与详细说明,帮助你快速上手并轻松掌握。

---

## 一、环境准备

1. **前提条件**  
   - 已安装 **Node.js(建议 v14+)**、**Yarn** 或 **npm**。  
   - 已安装并配置好 **Android Studio**(SDK 版本 29+)、**Xcode 12+(仅限 macOS)**。  
   - React Native 开发环境已经搭建完毕,可执行 `npx react-native run-android` 或 `npx react-native run-ios` 创建并运行空白项目。  

2. **ArcGIS 开发者账号与 API Key**  
   - 访问 [ArcGIS Developer](https://developers.arcgis.com/) 注册开发者账号。  
   - 在 Dashboard 中申请一个 **API Key**,后续在代码中用于初始化 ArcGIS 服务。  

3. **ArcGIS Runtime SDK**  
   - 本文使用 Esri 官方提供的 **ArcGIS Runtime SDK**,并结合社区维护的 React Native 组件 `react-native-arcgis-mapview`。  
   - 该组件底层封装了 Android 的 `com.esri.arcgisruntime.mapping.view.MapView` 与 iOS 的 `AGSMapView`,并对外暴露初始化、加载地图、SketchEditor、Graphic 等常用接口。  

4. **创建 RN 项目**  
   ```bash
   npx react-native init RNArcGISSketchDemo
   cd RNArcGISSketchDemo

二、安装与原生配置

2.1 安装 react-native-arcgis-mapview

# 使用 Yarn
yarn add react-native-arcgis-mapview

# 或 NPM
# npm install react-native-arcgis-mapview --save

此时,会在 node_modules/react-native-arcgis-mapview 目录下看到对应 Android 与 iOS 原生模块。

2.2 iOS 原生配置

  1. 进入 iOS 目录并安装 CocoaPods 依赖

    cd ios
    pod install --repo-update
    cd ..
  2. Info.plist 中添加 ArcGIS 权限及 API Key
    打开 ios/RNArcGISSketchDemo/Info.plist,添加下面几行:

    <!-- ArcGIS 许可与各种权限 -->
    <key>AGSAppClientID</key>
    <string>YOUR_ARCGIS_API_KEY</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>应用需要获取您的位置用于地图标绘</string>
    • YOUR_ARCGIS_API_KEY 替换为你在 ArcGIS Developer Dashboard 中生成的 API Key
    • NSLocationWhenInUseUsageDescription 是定位权限,用于在地图上显示用户当前位置及编辑周边要素。
  3. 确保 iOS Deployment Target

    • 在 Xcode 中选择项目 Target → GeneralDeployment Info → 将 iOS Deployment Target 设置为 13.0 及以上。

2.3 Android 原生配置

  1. 修改 android/app/build.gradle
    android/app/build.gradle 中,向 defaultConfig 中添加 ArcGIS API Key 配置:

    android {
        defaultConfig {
            applicationId "com.rnarcgissketchdemo"
            minSdkVersion 21
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
            // 添加以下一行
            manifestPlaceholders = [ "AGSCredentials:YOUR_ARCGIS_API_KEY" ]
        }
        ...
    }
    • 注意将 YOUR_ARCGIS_API_KEY 替换为实际值。
  2. 修改 android/app/src/main/AndroidManifest.xml
    <application> 标签中添加权限及 metadata:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.rnarcgissketchdemo">
    
        <!-- 地理定位权限 -->
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
        <application
            android:name=".MainApplication"
            android:label="@string/app_name"
            android:icon="@mipmap/ic_launcher"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:allowBackup="false"
            android:theme="@style/AppTheme">
    
            <!-- ArcGIS API Key -->
            <meta-data
                android:name="com.esri.arcgisruntime.ArcGISRuntime_LICENSE"
                android:value="${AGSCredentials}" />
    
            <activity
              android:name=".MainActivity"
              android:exported="true"
              android:label="@string/app_name">
              <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
            </activity>
        </application>
    </manifest>
  3. 编译验证

    npx react-native run-android
    • 确认 Android 模拟器或真机正常编译并启动,若出现 ArcGIS 许可错误,请检查 API Key 配置是否正确。

三、SketchEditorCtrl 核心概念

在 ArcGIS 中,Sketch Editor(要素绘制器)允许用户在地图上“绘制”或“编辑”点、线、面等几何要素。React Native 端使用的 SketchEditorCtrl 即是对原生 SketchEditor 的封装,常见功能包括:

  • 开始绘制:启动标绘模式,指定几何类型(点 Point、折线 Polyline、多边形 Polygon)。
  • 绘制过程:用户点击/拖动时,通过 SketchEditorCtrl 收集屏幕坐标并实时将结果渲染在地图上。
  • 完成绘制:触发 complete 事件,获取最终几何要素,通常以 GeoJSON 或 ArcGIS 几何对象形式返回,后续可用于保存到本地或发送服务端。
  • 编辑已绘制要素:可加载已有几何,切换到“编辑”模式,支持移动顶点、拉伸等操作。
  • 取消/撤销:支持撤销上一步操作、取消整个绘制。

3.1 SketchEditorCtrl 核心 API

以下示例展示 react-native-arcgis-mapview 对 Sketch Editor 的常用接口,并结合 React Native 组件的使用方式。

import ArcgisMapView, {
  ArcGISMapType,
  SketchEditorCtrl,
  GeometryType, // 'point' | 'polyline' | 'polygon'
  Graphic,      // 用于渲染绘制图形
} from 'react-native-arcgis-mapview';
  • SketchEditorCtrl.startDraw(geometryType, options)

    • geometryType:字符串,表示绘制类型,常见 'point' | 'polyline' | 'polygon'
    • options:可选参数对象,例如 { symbol: { color: '#FF0000', width: 3 } } 指定图形符号样式。
  • SketchEditorCtrl.stopDraw()

    • 停止当前绘制,隐藏辅助线及临时符号。
  • SketchEditorCtrl.clear()

    • 清除已绘制的所有图形。
  • SketchEditorCtrl.onDrawComplete(callback)

    • 绘制完成回调,函数参数为 geometry 对象,包含 typecoordinates 等信息。
  • SketchEditorCtrl.loadGeometry(geometry)

    • 加载并进入“编辑模式”,接收一个已存在的几何对象,用户可以修改顶点位置。
  • SketchEditorCtrl.onVertexChanged(callback)

    • 顶点移动时触发的回调,可用于 UI 协助(如实时显示当前坐标)。

四、核心代码示例

下面以一个“绘制并编辑多边形”功能为例,演示 React Native 端如何使用 SketchEditorCtrl。最终实现效果是:点击“开始绘制”按钮,进入绘制模式;用户在地图上点击或拖动绘制多边形;绘制完成后,显示 GeoJSON 信息;再点击“编辑多边形”按钮,可加载已绘制多边形并进行顶点移动;最后点击“清除”按钮,清空所有绘制。

4.1 完整示例代码(App.js)

// App.js
import React, { useRef, useState, useEffect } from 'react';
import {
  View,
  Text,
  Button,
  StyleSheet,
  Dimensions,
  SafeAreaView,
  ScrollView,
} from 'react-native';

import ArcgisMapView, {
  ArcGISMapType,
  SketchEditorCtrl,
  GeometryType,
  Graphic,
  GraphicLayerCtrl,
} from 'react-native-arcgis-mapview';

const { width, height } = Dimensions.get('window');

export default function App() {
  // 1. 保存当前绘制的几何(GeoJSON 形式)
  const [drawnGeometry, setDrawnGeometry] = useState(null);
  // 2. 保存控制器引用
  const sketchCtrl = useRef(null);
  const graphicLayerCtrl = useRef(null);

  // 3. 地图加载完成回调
  const onMapLoad = () => {
    console.log('地图加载完成');
  };

  // 4. 绘制完成回调
  const onDrawComplete = (geometry) => {
    console.log('绘制完成:', geometry);
    // geometry 示例:{ type: 'polygon', rings: [ [lng,lat], [lng,lat], ... ] }
    setDrawnGeometry(geometry);

    // 将几何转换为 Graphic 并添加到图层
    const graphic = new Graphic({
      geometry: geometry,
      symbol: {
        type: 'simple-fill',
        color: [255, 0, 0, 0.3],
        outline: {
          color: [255, 0, 0, 1],
          width: 2,
        },
      },
    });
    graphicLayerCtrl.current.addGraphic(graphic);
  };

  // 5. 开始绘制多边形
  const startDrawing = () => {
    // 清空图层
    graphicLayerCtrl.current.removeAllGraphics();
    setDrawnGeometry(null);

    sketchCtrl.current.startDraw(GeometryType.POLYGON, {
      // 可选:设置临时绘制时的符号样式
      symbol: {
        type: 'simple-fill',
        color: [0, 0, 255, 0.2],
        outline: {
          color: [0, 0, 255, 0.8],
          width: 2,
        },
      },
      // 可选:是否允许连续点击,默认 true
      allowdrawing: true,
    });
  };

  // 6. 停止绘制
  const stopDrawing = () => {
    sketchCtrl.current.stopDraw();
  };

  // 7. 编辑已绘制多边形
  const startEditing = () => {
    if (!drawnGeometry) {
      alert('请先绘制多边形');
      return;
    }
    // 加载几何进入编辑模式
    sketchCtrl.current.loadGeometry(drawnGeometry, {
      symbol: {
        type: 'simple-fill',
        color: [0, 255, 0, 0.2],
        outline: { color: [0, 255, 0, 1], width: 2 },
      },
    });
    // 监听顶点改变
    sketchCtrl.current.onVertexChanged((updatedGeometry) => {
      console.log('顶点改变:', updatedGeometry);
      setDrawnGeometry(updatedGeometry);
      // 更新图层:先清空再添加新的几何
      graphicLayerCtrl.current.removeAllGraphics();
      const g = new Graphic({
        geometry: updatedGeometry,
        symbol: {
          type: 'simple-fill',
          color: [0, 255, 0, 0.3],
          outline: { color: [0, 255, 0, 1], width: 2 },
        },
      });
      graphicLayerCtrl.current.addGraphic(g);
    });
  };

  // 8. 清除
  const clearAll = () => {
    sketchCtrl.current.stopDraw();
    graphicLayerCtrl.current.removeAllGraphics();
    setDrawnGeometry(null);
  };

  return (
    <SafeAreaView style={styles.safe}>
      <View style={styles.container}>
        {/* 1. ArcGIS 地图组件 */}
        <ArcgisMapView
          style={styles.map}
          mapType={ArcGISMapType.STREETS_VECTOR}
          onMapLoad={onMapLoad}
        >
          {/* 2. Graphic 图层:用来存放绘制完成的图形 */}
          <GraphicLayerCtrl ref={graphicLayerCtrl} />

          {/* 3. Sketch Editor 控制器 */}
          <SketchEditorCtrl
            ref={sketchCtrl}
            onDrawComplete={onDrawComplete}
          />
        </ArcgisMapView>

        {/* 4. 底部操作按钮 */}
        <View style={styles.toolbar}>
          <Button title="开始绘制" onPress={startDrawing} />
          <Button title="停止绘制" onPress={stopDrawing} />
          <Button title="编辑多边形" onPress={startEditing} />
          <Button title="清除所有" onPress={clearAll} />
        </View>

        {/* 5. 底部信息展示:当前绘制几何(JSON) */}
        <View style={styles.info}>
          <Text style={styles.infoTitle}>当前几何(GeoJSON):</Text>
          <ScrollView style={styles.infoScroll}>
            <Text style={styles.infoText}>
              {drawnGeometry
                ? JSON.stringify(drawnGeometry, null, 2)
                : '无内容'}
            </Text>
          </ScrollView>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safe: { flex: 1, backgroundColor: '#fff' },
  container: { flex: 1 },
  map: {
    width: width,
    height: height * 0.6,
  },
  toolbar: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingVertical: 8,
    backgroundColor: '#f0f0f0',
  },
  info: {
    flex: 1,
    padding: 8,
    backgroundColor: '#ffffff',
  },
  infoTitle: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  infoScroll: {
    marginTop: 4,
    backgroundColor: '#fafafa',
    borderColor: '#ddd',
    borderWidth: 1,
    borderRadius: 4,
    padding: 4,
  },
  infoText: {
    fontSize: 12,
    color: '#333',
  },
});

4.2 关键点详解

  1. ArcgisMapView 组件

    • mapType={ArcGISMapType.STREETS_VECTOR}:加载 ESRI 官方“街道矢量”底图。
    • onMapLoad={onMapLoad}:地图加载完成后才可调用绘制或图层操作。
  2. GraphicLayerCtrl 图层控制器

    • 用于承载所有绘制完成或编辑后的几何 Graphic 对象。
    • 通过 ref 调用 addGraphic()removeAllGraphics() 等方法管理图形集合。
  3. SketchEditorCtrl 控制器

    • 通过 ref 访问其方法:startDraw()stopDraw()loadGeometry()onDrawComplete()onVertexChanged()
    • onDrawComplete(geometry) 回调触发时,表示用户已经完成一次“点选-双击”或“完成按钮”操作,geometry 以 ArcGIS 几何对象格式返回,本文直接将其保存到 drawnGeometry,并转换为 Graphic 添加到图层。
  4. 绘制与编辑流程

    • 开始绘制:调用 sketchCtrl.current.startDraw(GeometryType.POLYGON, options) 进入多边形绘制模式。

      • 用户在地图上单击进行顶点添加,双击或长按结束绘制。
      • 临时几何会以半透明方式渲染(由 options.symbol 控制)。
    • 绘制完成onDrawComplete 拿到最终 geometry,再以新的 Graphic 对象渲染在图层上(使用红色或其他样式)。
    • 开始编辑:先判断 drawnGeometry 不为空,然后调用 sketchCtrl.current.loadGeometry(drawnGeometry, options),进入几何编辑模式。

      • 用户可以拖动顶点修改形状,此时 onVertexChanged 不断回调,返回更新后的几何,可以用来实时更新 GraphicLayer 或显示面积/周长等。
    • 停止绘制/退出编辑:调用 sketchCtrl.current.stopDraw() 退出当前绘制或编辑状态,但已添加到图层上的 Graphic 不受影响。
    • 清除所有:调用 sketchCtrl.current.stopDraw() 停止绘制,然后 graphicLayerCtrl.current.removeAllGraphics() 清空所有绘制结果,重置 drawnGeometry
  5. GeoJSON 信息展示

    • 在底部 ScrollView 中,通过 JSON.stringifydrawnGeometry(ArcGIS 原生几何格式)序列化,用于调试或复制到后端。
    • 示例几何格式可能如下:

      {
        "type": "polygon",
        "rings": [
          [116.391,39.907],
          [116.392,39.908],
          [116.390,39.908],
          [116.391,39.907]
        ]
      }
    • 开发者可根据需要转换为标准 GeoJSON 或 ArcGIS JSON。

五、图解:工作流程与组件交互

为了更直观地理解上面代码的执行逻辑,下面以简化顺序图形式展示各组件和控制器之间的交互流程。

┌────────────────────────────────────────────────────────┐
│               用户点击“开始绘制”按钮                 │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   App.js 中 startDrawing() 被调用                     │
│   → sketchCtrl.current.startDraw('polygon', options)  │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   SketchEditorCtrl 原生模块 启动“绘制模式”             │
│   • 在地图上拦截点击事件,添加临时顶点                    │
│   • 将临时几何实时渲染到 MapView (带半透明符号)       │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   用户在地图上连续点击/拖动完成多边形绘制               │
│   • 双击结束或点击“完成”                               │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   SketchEditorCtrl 触发 onDrawComplete(geometry) 回调│
│   • geometry 即最终几何                                │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   App.js 中 onDrawComplete() 处理:                    │
│   • setDrawnGeometry(geometry)                         │
│   • 创建 new Graphic({ geometry, symbol })             │
│   • graphicLayerCtrl.current.addGraphic(graphic)       │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   地图上新增一个“已完成多边形”图形(红色半透明)         │
└────────────────────────────────────────────────────────┘

-------------------------------------------------------------
┌────────────────────────────────────────────────────────┐
│               用户点击“编辑多边形”按钮                  │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   App.js 中 startEditing() 被调用                      │
│   → sketchCtrl.current.loadGeometry(drawnGeometry, options)  │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   SketchEditorCtrl 加载已有几何进入“编辑模式”             │
│   • 将已绘制多边形以绿色半透明符号渲染                     │
│   • 在顶点处显示可拖动的控制点                              │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   用户拖动顶点修改形状                                   │
│   • 每次顶点移动触发 onVertexChanged(updatedGeometry)   │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   App.js 中 onVertexChanged() 处理:                    │
│   • setDrawnGeometry(updatedGeometry)                   │
│   • graphicLayerCtrl.current.removeAllGraphics()        │
│   • graphicLayerCtrl.current.addGraphic(新 geometry Graphic) │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   地图上“多边形”实时更新为用户拖动后的新形状(绿色半透明)  │
└────────────────────────────────────────────────────────┘

-------------------------------------------------------------
┌────────────────────────────────────────────────────────┐
│               用户点击“清除所有”按钮                    │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   App.js 中 clearAll() 被调用                            │
│   • sketchCtrl.current.stopDraw()                       │
│   • graphicLayerCtrl.current.removeAllGraphics()        │
│   • setDrawnGeometry(null)                              │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│   地图上所有绘制内容与临时图形全部清除                     │
└────────────────────────────────────────────────────────┘
  1. 事件触发:按钮点击 → 调用对应方法(开始绘制/编辑/清除)。
  2. SketchEditorCtrl:原生模块负责拦截地图触摸事件、生成几何并通知 JS。
  3. GraphicLayerCtrl:在 JS 层用 Graphic 对象渲染最终几何到地图图层。
  4. State 更新:通过 useState 保存几何,并在 UI 底部展示 GeoJSON 信息。

六、常见疑难与优化建议

  1. 地图偶尔不响应触摸绘制

    • 确保 SketchEditorCtrl 已经正确初始化并在地图加载完成后才调用 startDraw()
    • 检查是否在 onMapLoad 回调后再执行 startDraw,否则地图还未完全加载时拦截事件会无效。
  2. 多边形边界不封闭

    • SketchEditorCtrl 默认会自动将最后一个顶点与第一个顶点相连,确保闭合。如果发现不闭合,检查 GeometryType 是否正确设置为 'polygon'
  3. 编辑模式闪烁或多次添加图形

    • onVertexChanged 回调中,需先清空图层再添加新的 Graphic,否则会出现残留旧图形叠加。
    • 可根据需求在 onVertexChanged 中加防抖(Debounce)处理,避免连续快速回调导致性能问题。
  4. 大数据量绘制性能

    • 如果一次性绘制大量点/线/面,建议将要素分批添加,并在视图级别控制可见图层。
    • ArcGIS Runtime SDK 会对大量矢量要素进行批量渲染优化,但在移动端仍要注意合理简化几何。
  5. 与 GPS 定位结合

    • 可通过 React Native 的 react-native-geolocation-serviceLocation API 获取定位,再将坐标转换为 ArcGIS 坐标系(WGS84 → WebMercator),手动调用 SketchEditorCtrl.loadGeometry 或动态添加 Graphic 实时定位。
    • ArcGIS GeoView 自带 LocationDisplay 模块,不过在 React Native 端需要使用原生模块或自行桥接。
  6. 离线地图与本地切片

    • 如果需要在无网络环境下使用地图,可下载 ArcGIS 离线切片包 (Tile Package),并在 ArcgisMapView 中指定离线切片路径:

      <ArcgisMapView
        style={styles.map}
        mapType={ArcGISMapType.LOCAL_TILE}
        localTilePath={'/sdcard/arcgis/tiles.tpk'}
        onMapLoad={onMapLoad}
      />
    • 然后在 SketchEditorCtrl 中照常绘制,所有几何操作都与离线地图兼容。

七、总结

本文通过“React Native 携手 ArcGIS:SketchEditorCtrl 地图开发实战”示例,从环境配置细节到核心代码拆解、流程图解、常见问题与优化建议,详细剖析了如何在 React Native 应用中集成 ArcGIS 地图,并使用 SketchEditorCtrl 完成“绘制多边形→编辑多边形→清除”整体流程。

  • 环境准备:注册 ArcGIS 开发者账号,获取 API Key,配置 Android/iOS 原生文件。
  • 核心组件ArcgisMapViewSketchEditorCtrlGraphicLayerCtrl 三个 React Native 组件,以及 GeometryTypeGraphic 等辅助类。
  • 绘制与编辑流程

    1. startDraw(GeometryType.POLYGON) 进入绘制模式 → 用户点击地图绘制 → onDrawComplete(geometry) 回调。
    2. 生成 Graphic 对象并添加到图层 → 地图渲染多边形。
    3. loadGeometry(geometry) 进入编辑模式 → onVertexChanged(updatedGeometry) 实时回调 → 更新 Graphic
    4. stopDraw() 停止绘制/编辑;removeAllGraphics() 清除全部。
  • 核心代码示例App.js 提供完整示例,涵盖全部 API 调用、状态管理与 UI 交互。
  • 图解流程:用简化顺序图分别说明“开始绘制→绘制完成→编辑→清除”的事件流与组件交互逻辑。
  • 常见问题与优化:包括地图触摸无响应、多边形闭合、编辑闪烁、大数据量性能优化、GPS 定位结合、离线地图兼容等实战要点。

掌握以上技术后,你可以在 React Native 应用中实现更丰富的地图交互功能,如:

  • 现场地理信息采集:用户可在户外通过手绘方式快速标记地物边界,并上传到后端云端。
  • 资源规划与测量:结合 SketchEditor 提供面积、周长计算,实现地块测量与管理。
  • 轨迹记录与回放:使用折线(GeometryType.POLYLINE)记录用户路径,并通过 timeStamp 字段实现地图轨迹回放。
  • 复杂要素编辑:加载 CAD 转换的矢量要素,提供移动顶点、拉伸、剪切等编辑功能。

通过本文示例与思路,你应具备了在 React Native 环境下快速集成 ArcGIS 地图与 Sketch Editor 的能力。接下来,可以根据项目需求,自行扩展点、线、面的符号样式、测量工具、属性编辑器、图层管理、空间分析等功能,打造更为强大的 GIS 移动应用。

React Native 状态管理深度剖析

在构建 React Native 应用时,状态管理 是核心话题之一。无论是简单的本地组件状态,还是跨多个页面、甚至全局共享的状态,都直接影响应用的可维护性、性能和可扩展性。本文将从状态管理的基本概念入手,详细剖析 React Native 中常见的状态管理方案,包括:

  1. 本地组件状态:useStateuseReducer
  2. Context API:适用于轻量级全局状态
  3. Redux:最常用的集中式状态管理
  4. MobX:基于可观察数据的响应式状态管理
  5. Recoil:Facebook 出品的现代状态管理方案
  6. Zustand & React Query 等轻量方案

每一部分都包含原理解析代码示例图解,帮助你迅速理解并在项目中灵活运用。


目录

  1. 状态管理概述
  2. 本地组件状态(useState 与 useReducer)
  3. Context API:轻量级全局共享
  4. Redux:集中式状态管理

    1. 原理解析
    2. 安装与基本配置
    3. Action、Reducer 与 Store
    4. React Redux 连接组件
    5. 中间件:Redux Thunk / Saga
    6. 代码示例
    7. 状态流图解
  5. MobX:基于可观察数据的响应式方案

    1. 原理解析
    2. 安装与配置
    3. 可观察(observable)与动作(action)
    4. 代码示例
    5. 响应式更新图解
  6. Recoil:Facebook 出品的现代状态管理

    1. 原理解析
    2. 安装与配置
    3. Atom 与 Selector
    4. 代码示例
    5. 数据流图解
  7. Zustand:更轻量的状态管理

    1. 原理解析
    2. 安装与配置
    3. 代码示例
  8. React Query:数据获取与缓存管理

    1. 原理解析
    2. 安装与配置
    3. 代码示例
  9. 如何选择合适的方案?
  10. 总结

1. 状态管理概述

在 React(包括 React Native)应用中,“状态”指的是影响界面呈现的一切动态数据,例如用户输入、网络请求结果、导航路由、全局配置、鉴权信息等。状态管理 则是指如何存储、读取、更新以及订阅这些动态数据。

常见需求包括:

  • 局部状态:只在一个组件内部使用,例如表单输入字段的内容、动画播放状态等
  • 全局状态:在多个组件之间共享,例如用户登入状态、主题色、购物车数据等
  • 异步数据:从后端获取的网络数据,需要做加载、缓存、错误处理
  • 衍生状态:基于现有状态计算而来,例如过滤后的列表、分页后的数据

不同场景下,我们需要不同粒度的状态管理方案:

  1. 组件内部状态:用 useStateuseReducer 足够
  2. 跨组件共享但轻量:Context API 配合 useReducer即可
  3. 复杂业务、多人协作、需要中间件:推荐 Redux
  4. 响应式、面向对象风格:MobX
  5. 现代 Hooks 与 DSL:Recoil、Zustand
  6. 数据获取 & 缓存管理:React Query

下面分别讲解每种方案的原理、优势与劣势,并附上代码示例及图解。


2. 本地组件状态(useState 与 useReducer)

2.1 useState

useState 是函数组件管理简单局部状态的最常用 Hook。示例:

import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <View style={{ alignItems: 'center', marginTop: 50 }}>
      <Text style={{ fontSize: 24 }}>当前计数:{count}</Text>
      <Button title="增加" onPress={() => setCount(count + 1)} />
      <Button title="重置" onPress={() => setCount(0)} />
    </View>
  );
}
  • 原理useState 在组件首次渲染时会创建一个内部存储槽,保存初始状态。后续每次调用 setCount,React 会将新的 count 存入该槽并触发组件重渲染。
  • 适用场景:简单标量、布尔、字符串、数组、对象等本地状态,无需复杂逻辑时优先考虑。

2.2 useReducer

当状态逻辑复杂,涉及多个子值或者下一个状态依赖前一个状态时,推荐用 useReducer。示例:

import React, { useReducer } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

// 定义 reducer 函数
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error(`未知 action: ${action.type}`);
  }
}

export default function CounterWithReducer() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <View style={styles.container}>
      <Text style={styles.text}>当前计数:{state.count}</Text>
      <View style={styles.buttons}>
        <Button title="增加" onPress={() => dispatch({ type: 'increment' })} />
        <Button title="减少" onPress={() => dispatch({ type: 'decrement' })} />
        <Button title="重置" onPress={() => dispatch({ type: 'reset' })} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { alignItems: 'center', marginTop: 50 },
  text: { fontSize: 24, marginBottom: 20 },
  buttons: { flexDirection: 'row', justifyContent: 'space-between', width: 250 },
});
  • 原理useReducer 接受一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。dispatch 接受一个 action,对应 reducer 返回新状态。
  • 适用场景:状态逻辑复杂,或多个状态值有依赖时。例如:表单状态管理(验证、提交等)、购物车添加/删除逻辑、游戏状态机等。

3. Context API 轻量级全局共享

当需要跨若干个深层组件共享状态,但项目不想引入 Redux 时,可使用 Context API。Context 通过组件树传递值,避免逐层传递 props。

3.1 创建 Context

// src/contexts/AuthContext.js
import React, { createContext, useState } from 'react';

export const AuthContext = createContext({
  user: null,
  login: () => {},
  logout: () => {},
});

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (username) => setUser({ username });
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
  • AuthContext.Provideruserloginlogout 暴露给整个子树。

3.2 在组件中使用

// src/screens/ProfileScreen.js
import React, { useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { AuthContext } from '../contexts/AuthContext';

export default function ProfileScreen() {
  const { user, logout } = useContext(AuthContext);

  return (
    <View style={styles.container}>
      {user ? (
        <>
          <Text style={styles.text}>欢迎,{user.username}!</Text>
          <Button title="退出登录" onPress={logout} />
        </>
      ) : (
        <Text style={styles.text}>请先登录</Text>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  text: { fontSize: 18 },
});

在根组件包裹提供者:

// App.js
import React from 'react';
import { AuthProvider } from './src/contexts/AuthContext';
import ProfileScreen from './src/screens/ProfileScreen';

export default function App() {
  return (
    <AuthProvider>
      <ProfileScreen />
    </AuthProvider>
  );
}

图解:Context 数据流

┌────────────────────────────┐
│        AuthProvider        │
│  user, login, logout 值存在 │
└────────────────────────────┘
           │
           ▼
┌────────────────────────────┐
│    ProfileScreen & Siblings │
│  useContext(AuthContext)    │
└────────────────────────────┘
  • Context 可在任意深度的子组件中直接获取,无需逐层传递 props。
  • 注意:Context 过度使用会导致组件重渲染范围过大,性能受影响。仅在真正需要跨多层共享时使用。

4. Redux 集中式状态管理

Redux 是最常见、最成熟的集中式状态管理方案。它将整个应用的状态存储在一个统一的 Store 中,通过Action → Reducer → Store 的模式更新数据,并通过 订阅(subscribe / react-redux) 驱动 UI 更新。

4.1 原理解析

  1. Store:一个包含全局状态树的对象,只能通过 dispatch Action 来更新。
  2. Action:一个普通对象,描述“发生了什么”,至少包含 type 字段,可携带 payload
  3. Reducer:一个纯函数,接收当前 state 和 action,返回新的 state。
  4. Dispatch:向 Store 发送 Action,触发 Reducer 更新状态。
  5. 订阅(subscribe/mapStateToProps):React-Redux 将 Store 的 state 通过 mapStateToPropsuseSelector 绑定到组件,当 state 更新时,组件自动重新渲染。

Redux 数据流图解:

┌────────────┐
│ Component  │
│ dispatch() │
└─────┬──────┘
      │ Action
      ▼
┌────────────┐
│  Store     │
│  Reducer   │<── current State + Action → new State
│            │
└─────┬──────┘
      │ 订阅通知
      ▼
┌────────────┐
│ Components │ 重新读取新 State 渲染 UI
└────────────┘

4.2 安装与基本配置

yarn add redux react-redux
# 如果需要异步处理
yarn add redux-thunk

4.2.1 创建 Store

// src/store/index.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

// 1. 导入 Reducer
import authReducer from './reducers/auth';
import dataReducer from './reducers/data';

// 2. 合并 Reducer
const rootReducer = combineReducers({
  auth: authReducer,
  data: dataReducer,
});

// 3. 创建 Store,应用中间件
const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;

4.2.2 连接根组件

// App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './src/store';
import MainNavigator from './src/navigation/MainNavigator';

export default function App() {
  return (
    <Provider store={store}>
      <MainNavigator />
    </Provider>
  );
}
  • Provider 使整个组件树能够访问 store

4.3 Action、Reducer 与 Store

4.3.1 定义 Action Types 与 Action Creators

// src/store/actions/authActions.js
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGOUT = 'LOGOUT';

export const login = (username) => (dispatch) => {
  // 异步示例:模拟登录接口
  setTimeout(() => {
    dispatch({ type: LOGIN_SUCCESS, payload: { username } });
  }, 1000);
};

export const logout = () => ({ type: LOGOUT });

4.3.2 定义 Reducer

// src/store/reducers/auth.js
import { LOGIN_SUCCESS, LOGOUT } from '../actions/authActions';

const initialState = {
  user: null,
  loading: false,
};

export default function authReducer(state = initialState, action) {
  switch (action.type) {
    case LOGIN_SUCCESS:
      return { ...state, user: action.payload.username };
    case LOGOUT:
      return { ...state, user: null };
    default:
      return state;
  }
}

4.4 React Redux 连接组件

4.4.1 useSelector 与 useDispatch

React-Redux 提供了两个 Hook 来绑定 Redux 状态与 dispatch:

  • useSelector(selector):选择需要的 state 片段,并订阅更新。
  • useDispatch():返回 dispatch 函数,用于分发 Action。
// src/screens/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { login, logout } from '../store/actions/authActions';

export default function LoginScreen() {
  const dispatch = useDispatch();
  const user = useSelector((state) => state.auth.user);
  const [username, setUsername] = useState('');

  const handleLogin = () => {
    dispatch(login(username));
  };

  const handleLogout = () => {
    dispatch(logout());
  };

  return (
    <View style={styles.container}>
      {user ? (
        <>
          <Text style={styles.text}>欢迎,{user}!</Text>
          <Button title="退出登录" onPress={handleLogout} />
        </>
      ) : (
        <>
          <TextInput
            style={styles.input}
            placeholder="请输入用户名"
            value={username}
            onChangeText={setUsername}
          />
          <Button title="登录" onPress={handleLogin} />
        </>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  text: { fontSize: 24, marginBottom: 20 },
  input: {
    width: '80%',
    height: 44,
    borderColor: '#CCC',
    borderWidth: 1,
    marginBottom: 12,
    paddingHorizontal: 8,
  },
});
  • useSelector 自动订阅 Redux Store,当 auth.user 改变时,组件会重新渲染。
  • useDispatch 用于派发 loginlogout 等异步或同步 Action。

4.5 中间件:Redux Thunk / Redux Saga

如果需要在 Action 中进行异步操作(如网络请求),常用中间件有:

  1. Redux Thunk

    • 允许 Action Creator 返回一个函数 (dispatch, getState) => { ... },内部可执行异步逻辑,再根据结果 dispatch 普通 Action。
  2. Redux Saga

    • 基于 Generator 函数,监听指定 Action,然后在 Saga 中处理异步调用(call/put/select),对复杂异步逻辑有更好的组织能力。

本文仅展示 Thunk 示例,如果需进一步了解 Saga,可另行查阅。

4.6 完整代码示例:Todo 应用

下面以一个简单的 Todo List 应用为例,演示 Redux 流程完整样式。

4.6.1 Action 与 Reducer

// src/store/actions/todoActions.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';

export const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: Date.now().toString(), text },
});

export const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: { id },
});
// src/store/reducers/todoReducer.js
import { ADD_TODO, TOGGLE_TODO } from '../actions/todoActions';

const initialState = { todos: [] };

export default function todoReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        todos: [
          ...state.todos,
          { id: action.payload.id, text: action.payload.text, completed: false },
        ],
      };
    case TOGGLE_TODO:
      return {
        todos: state.todos.map((todo) =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    default:
      return state;
  }
}

4.6.2 Store 配置

// src/store/index.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import todoReducer from './reducers/todoReducer';
import authReducer from './reducers/auth';

const rootReducer = combineReducers({
  todos: todoReducer,
  auth: authReducer,
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;

4.6.3 TodoList 组件

// src/screens/TodoListScreen.js
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  Button,
  FlatList,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { addTodo, toggleTodo } from '../store/actions/todoActions';

export default function TodoListScreen() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.todos);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Todo List (Redux)</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="输入待办项"
          value={text}
          onChangeText={setText}
        />
        <Button
          title="添加"
          onPress={() => {
            if (text.trim()) {
              dispatch(addTodo(text));
              setText('');
            }
          }}
        />
      </View>
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity
            style={styles.item}
            onPress={() => dispatch(toggleTodo(item.id))}
          >
            <Text style={item.completed ? styles.doneText : styles.text}>
              {item.text}
            </Text>
          </TouchableOpacity>
        )}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  inputRow: { flexDirection: 'row', marginBottom: 12 },
  input: {
    flex: 1,
    borderColor: '#CCC',
    borderWidth: 1,
    borderRadius: 4,
    marginRight: 8,
    paddingHorizontal: 8,
    height: 44,
  },
  item: {
    paddingVertical: 12,
    borderBottomColor: '#EEE',
    borderBottomWidth: 1,
  },
  text: { fontSize: 16 },
  doneText: { fontSize: 16, textDecorationLine: 'line-through', color: '#999' },
});
  • 点击“添加”会 dispatch(addTodo),将新的 TODO 存入 Redux;
  • 点击某项会 dispatch(toggleTodo),切换完成状态。

4.6.4 总体流程图解

┌──────────────┐
│  用户输入“吃饭”  │
└───────┬──────┘
        │ dispatch({ type: 'ADD_TODO', payload: { id: '123', text: '吃饭' } })
        ▼
┌──────────────────┐
│  Redux Middleware │ (thunk,无异步)
└───────┬──────────┘
        │
        ▼
┌──────────────────┐
│    Reducer       │
│ state.todos: []  │
│ + action → 新 state: { todos: [{ id: '123', text: '吃饭', completed: false }] }
└───────┬──────────┘
        │
        ▼
┌──────────────────┐
│  React-Redux 订阅 │
│ FlatList 自动更新 │
└──────────────────┘
  • Redux 确保所有操作可预测、纯粹,并可通过工具调试。

5. MobX 基于可观察数据的响应式方案

MobX 通过可观察(observable)动作(action),实现响应式状态管理。每个可观察状态改变时,使用它的组件会自动重新渲染,类似 Vue 的响应式。

5.1 原理解析

  1. 可观察(observable)

    • 任何变量(对象、数组、Map、Class 属性等)都可标记为 observable,当发生变化时,依赖它的组件自动更新。
  2. 动作(action)

    • 修改可观察状态的函数必须标记为 action,以便 MobX 在事务中跟踪变化。
  3. 观察者(observer)

    • observer 高阶组件(或 Hook)包裹的 React 组件会成为观察者,使用到 observable 值时会自动订阅,发生变化时触发 render。

MobX 数据流图解:

┌─────────────────┐     ┌────────────────────────┐
│ observable data │ ──> │ observer Component     │
└─────────────────┘     └────────────────────────┘
        ^                           │
        │ action 修改 observable     │
        └───────────────────────────┘

5.2 安装与配置

yarn add mobx mobx-react-lite

5.2.1 创建 Store(Class 或 Hook)

// src/stores/counterStore.js
import { makeAutoObservable } from 'mobx';

class CounterStore {
  count = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }

  reset() {
    this.count = 0;
  }
}

export const counterStore = new CounterStore();
  • makeAutoObservable(this) 会将类实例的所有属性标记为可观察,并将所有方法标记为 action。

5.2.2 在组件中使用

// src/screens/CounterWithMobX.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { observer } from 'mobx-react-lite';
import { counterStore } from '../stores/counterStore';

const CounterWithMobX = observer(() => {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>当前计数:{counterStore.count}</Text>
      <View style={styles.row}>
        <Button title="增加" onPress={() => counterStore.increment()} />
        <Button title="减少" onPress={() => counterStore.decrement()} />
        <Button title="重置" onPress={() => counterStore.reset()} />
      </View>
    </View>
  );
});

export default CounterWithMobX;

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  text: { fontSize: 24, marginBottom: 20 },
  row: { flexDirection: 'row', justifyContent: 'space-around', width: 250 },
});
  • observer 让组件订阅 counterStore.count,当其变化时重新渲染。
  • counterStore.increment() 是 action,会自动批量更新状态并触发订阅。

5.3 响应式更新图解

┌───────────────────────┐
│    counterStore.count │──┐
└───────────────────────┘  │
         ▲                 │
         │ observer 订阅    │
         │                 ▼
┌───────────────────────┐  ┌────────────────────────┐
│   CounterWithMobX     │  │ React 原生渲染引擎      │
│ <Text>{count}</Text>  │  │    更新 UI              │
└───────────────────────┘  └────────────────────────┘
         ▲
         │ action
         │ counterStore.increment()
         │
┌───────────────────────┐
│     makeAutoObservable│
│    标记为可观察/动作   │
└───────────────────────┘
  • MobX 的响应式机制基于 Getter/Setter、Proxy 等技术,一旦 count 变化,CounterWithMobX 会自动重新渲染。

6. Recoil(Facebook 出品的现代状态管理)

Recoil 是 Facebook 开源的状态管理库,专为 React(包括 React Native)设计,使用Atom 表示可写可读的最小状态单元,Selector 表示基于 Atom 或其他 Selector 的派生状态,具备并发模式异步查询等特性。

6.1 原理解析

  1. Atom

    • 原子状态,可通过 useRecoilState 订阅与更新;多个组件使用同一个 Atom 时,共享同一份状态。
  2. Selector

    • 派生状态或异步数据查询,将多个 Atom 组合或从后端获取数据;使用 useRecoilValue 读取其值。当依赖的 Atom 更新时,Selector 会重新计算。
  3. RecoilRoot

    • 包裹应用,提供 Recoil 状态环境。

Recoil 数据流图解:

┌──────────────────┐     ┌──────────────────┐
│      Atom A      │     │      Atom B      │
└──────────────────┘     └──────────────────┘
         │                        │
         └─────→ Selector C ←─────┘  (依赖 A、B 或 异步Fetch)
                   ↓
             React 组件 使用 C

6.2 安装与配置

yarn add recoil

6.2.1 根组件包裹

// App.js
import React from 'react';
import { RecoilRoot } from 'recoil';
import CounterRecoil from './src/screens/CounterRecoil';

export default function App() {
  return (
    <RecoilRoot>
      <CounterRecoil />
    </RecoilRoot>
  );
}

6.3 Atom与Selector

6.3.1 定义 Atom

// src/state/counterAtom.js
import { atom } from 'recoil';

export const counterAtom = atom({
  key: 'counterAtom', // 唯一 ID
  default: 0,         // 默认初始值
});

6.3.2 定义 Selector(派生状态示例)

// src/state/counterSelector.js
import { selector } from 'recoil';
import { counterAtom } from './counterAtom';

export const doubleCounterSelector = selector({
  key: 'doubleCounterSelector',
  get: ({ get }) => {
    const count = get(counterAtom);
    return count * 2;
  },
});

6.4 代码示例

// src/screens/CounterRecoil.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useRecoilState, useRecoilValue } from 'recoil';
import { counterAtom } from '../state/counterAtom';
import { doubleCounterSelector } from '../state/counterSelector';

export default function CounterRecoil() {
  const [count, setCount] = useRecoilState(counterAtom);
  const doubleCount = useRecoilValue(doubleCounterSelector);

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Count: {count}</Text>
      <Text style={styles.text}>Double Count: {doubleCount}</Text>
      <View style={styles.row}>
        <Button title="增加" onPress={() => setCount(count + 1)} />
        <Button title="减少" onPress={() => setCount(count - 1)} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  text: { fontSize: 20, marginVertical: 8 },
  row: { flexDirection: 'row', width: 200, justifyContent: 'space-between' },
});
  • useRecoilState(counterAtom) 返回 [count, setCount]
  • useRecoilValue(doubleCounterSelector) 自动订阅 counterAtom,当其变化时重新计算 doubleCount 并更新 UI。

7. Zustand 更轻量的状态管理

Zustand 是一个更轻量、API 简洁的状态管理库,基于 Hooks,无需样板代码。

7.1 原理解析

  1. create

    • 通过 create 创建一个全局 store,内部使用原生可观察(subscribe)机制,无需 Provider。
  2. useStore Hook

    • 返回状态与 actions,组件调用时自动订阅所使用的状态片段。

Zustand 数据流图解:

┌─────────────────────────────┐
│   create((set, get) => ...) │
│  → 返回 useStore Hook       │
└─────────────────────────────┘
            │
            ▼
┌─────────────────────────────┐
│   useStore(state => state.x)│  ← 组件订阅 x
└─────────────────────────────┘
            ▲
            │ action 调用 set → 更新状态,触发订阅
            │
┌─────────────────────────────┐
│     state = { ... }         │
└─────────────────────────────┘

7.2 安装与配置

yarn add zustand

7.2.1 创建 Store

// src/store/zustandStore.js
import create from 'zustand';

export const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));
  • create 接受一个函数,函数参数 (set, get),返回一个包含状态和操作的对象。

7.2.2 在组件中使用

// src/screens/CounterZustand.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useCounterStore } from '../store/zustandStore';

export default function CounterZustand() {
  const { count, increment, decrement, reset } = useCounterStore((state) => ({
    count: state.count,
    increment: state.increment,
    decrement: state.decrement,
    reset: state.reset,
  }));

  return (
    <View style={styles.container}>
      <Text style={styles.text}>当前计数:{count}</Text>
      <View style={styles.row}>
        <Button title="增加" onPress={increment} />
        <Button title="减少" onPress={decrement} />
        <Button title="重置" onPress={reset} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  text: { fontSize: 24, marginBottom: 20 },
  row: { flexDirection: 'row', justifyContent: 'space-around', width: 250 },
});
  • useCounterStore(state => ({ ... })) 只订阅所需的属性,性能友好。

8. React Query:数据获取与缓存管理

虽然不专门用于“UI 状态”管理,但 React Query 在服务端状态(数据获取、缓存、刷新)方面表现卓越。将其与上述状态管理方案结合,可形成全方位的状态解决方案。

8.1 原理解析

  1. 查询缓存(Query Cache)

    • 对网络请求进行缓存、去重、过期等管理。
  2. 自动重新触发

    • 当组件挂载时自动拉取数据;当数据失效或聚焦时重新拉取。
  3. Mutation 管理

    • 提供对数据变更(POST、PUT、DELETE)的抽象,并支持乐观更新。

React Query 数据流图解:

┌────────────────────────────┐
│ useQuery('todos', fetch)   │
└───────┬────────────────────┘
        │
        ▼
┌────────────────────────────┐
│  Query Cache (key=todos)   │
│  • 若缓存存在且未过期 → 返回  │
│  • 否 → 发起 fetch 请求       │
└───────┬────────────────────┘
        │ fetch 成功
        ▼
┌────────────────────────────┐
│ 更新缓存并触发订阅组件渲染   │
└────────────────────────────┘

8.2 安装与使用

yarn add @tanstack/react-query

8.2.1 在根组件配置 QueryClient

// App.js
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import TodoListScreen from './src/screens/TodoListScreen';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TodoListScreen />
    </QueryClientProvider>
  );
}

8.2.2 示例:获取 TODO 列表

// src/screens/TodoListScreen.js
import React from 'react';
import { View, Text, FlatList, StyleSheet, ActivityIndicator } from 'react-native';
import { useQuery } from '@tanstack/react-query';

// 模拟 API
async function fetchTodos() {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10');
  return response.json();
}

export default function TodoListScreen() {
  const { data, error, isLoading, isError } = useQuery(['todos'], fetchTodos);

  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (isError) {
    return (
      <View style={styles.center}>
        <Text>Error: {error.message}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => `${item.id}`}
      renderItem={({ item }) => (
        <View style={styles.item}>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  item: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#EEE' },
});
  • useQuery(['todos'], fetchTodos) 首次挂载时调用 fetchTodos,并缓存结果;
  • 当组件再次挂载或参数变化,会根据缓存策略决定是否重新请求。

9. 如何选择合适的方案?

不同项目规模、业务复杂度和团队偏好决定最佳方案。下面给出几点参考建议:

  1. 项目较小、团队熟练 Hooks

    • 只需 useState + Context 即可;若仅需一个简单的全局状态(鉴权、主题切换),Context + useReducer 性能与维护成本最小。
  2. 中大型项目、多人协作、需要可视化调试

    • Redux 是最成熟的解决方案,拥有丰富的生态(Redux DevTools、Middlewares、Redux Toolkit、Redux Persist 等),适合复杂业务。
  3. 需要响应式开发、面向对象风格

    • MobX 使得状态与 UI 响应式耦合较好,上手简单,适合那些倾向于“类 + 装饰器”语法的团队。但要注意飙升的可观察数据量会增加调试难度。
  4. 追求现代化 Hook 体验

    • Recoil 提供了 Atom/Selector 的 DSL,原生支持并发模式与异步数据流,适合对 React 性能有更高要求的团队。
  5. 轻量 & 无 Provider

    • Zustand 极简 API,适合要快速上手,无需写大量模板代码的场景。
  6. 数据获取 & 缓存管理

    • 对于服务端数据React Query(或 SWR)是最佳选择,与上述任何状态管理方案结合都很自然。
方案典型规模学习曲线优点缺点
useState/useReducer + Context小型无额外依赖、易上手、轻量随项目增多,Context 参数膨胀、性能难优化
Redux中大型中等可视化调试、丰富生态、社区成熟Boilerplate 较多、样板代码多
MobX中大型中等响应式自动更新、面向对象风格可观察链路复杂时性能调优较难
Recoil中型-大型中等原生 Hook、并发安全、异步支持生态相对年轻、社区资源偏少
Zustand小型-中型API 极简、无 Provider、轻量无丰富插件生态、纯 JS 管理需谨慎
React Query所有规模专注数据获取与缓存、自动重试仅服务端数据,不适合 UI 状态管理

10. 总结

本文从基础到进阶,全面剖析了 React Native 中常见的状态管理方案:

  1. 本地组件状态useStateuseReducer
  2. Context API:轻量级全局状态共享
  3. Redux:集中式、可视化、可扩展、适合复杂场景
  4. MobX:基于可观察数据的响应式方案
  5. Recoil:Facebook 出品的现代 Hook 状态管理
  6. Zustand:更轻量无 Provider 的方案
  7. React Query:专注服务端数据获取与缓存管理

每种方案都有其适用场景与优缺点,关键在于根据项目规模、团队技术栈与业务需求 做出合理选择。常见做法是:

  • 让小型项目直接用 useState + Context;
  • 中大型项目用 Redux 管理全局状态,React Query 管理网络请求;
  • 希望更简洁或响应式开发的团队,可以尝试 MobX、Recoil 或 Zustand。

不论你选择哪种方案,都要牢记核心原则:状态驱动视图。良好的状态管理方案能让你的 React Native 应用更易维护、更具可读性、更高性能。希望这篇深度剖析能帮助你更好地理解并运用各种状态管理技术,为项目保驾护航。愿你在 React Native 的开发道路上越走越顺,打造出高质量的移动应用!

# React Native导航新选择:Redux Router,打造现代化移动应用导航

在 React Native 社区中,`react-navigation` 和 `react-native-navigation` 一直是主流的导航方案。但随着应用复杂度提升,我们希望将导航状态与全局状态管理(Redux)深度集成,方便在 Redux DevTools 中查看、回溯、调试,以及结合中间件做登录鉴权、权限控制等场景。此时,**Redux Router**(或更常用的现代化版本 `redux-first-router`)便成为了一种“新选择”。本文将从安装、流程、代码示例、状态管理、常见使用场景等方面,配合图解与详细说明,带你快速上手在 React Native 中使用 Redux Router 构建导航。

---

## 一、为什么要用 Redux Router?

1. **导航状态与 Redux 同步**  
   - 将导航(路由)状态纳入 Redux Store,所有页面跳转都会体现在同一个 state 树中。  
   - 方便使用 Redux DevTools 回溯历史、记录操作、回滚状态,进行可视化调试。

2. **统一业务逻辑、中间件**  
   - 在跳转前可通过 Redux 中间件拦截,执行鉴权、日志记录、异步加载数据等。  
   - 比如:进入某个需要登录的页面时,在 middleware 中判断用户是否已登录,如未登录可自动跳转到登录页。

3. **Web 与 RN 代码复用**  
   - `redux-first-router` 支持 React Web,同时在 React Native 上也可复用大量配置(如配置路由表、action type、reducer、middleware),提升团队效率。

4. **更强的可控性**  
   - 自定义路由行为更灵活,可对跳转动作(action)附加元数据(meta),结合 Saga/Thunk 进行异步导航。

---

## 二、核心概念与架构图解

在开始编码前,我们先从整体架构上理解 Redux Router 在 React Native 中的工作流程。

┌──────────────────────────────────────────────────────────────────┐
│ 用户交互 │
│ 比如:点击按钮 dispatch(push({ type: 'HOME', payload: { ... }})) │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ Redux Action 发出 │
│ { type: 'HOME', payload: { ...}, meta: { skip: false} } │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ redux-first-router Middleware 拦截 │
│ - 根据 routesMap 匹配 action.type → 找到对应路由 │
│ - 生成新的 location 对象,写入 store.locationReducers │
│ - 如果 meta.skip: true,则跳过原生导航逻辑 │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ Redux Store 更新 location 状态 │
│ store.location: { │
│ pathname: '/home', │
│ type: 'HOME', │
│ payload: { ... } │
│ } │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ React-Redux 连接到 locationReducer 的 Router 组件 │
│ 根据新的 location.pathname,渲染对应的 React Native Stack/Tab │
│ Navigator → 展示新的页面组件 │
└──────────────────────────────────────────────────────────────────┘


- **routesMap**:定义 action type 与 route(path) 的对应关系,例如 `HOME: '/home'`。  
- **locationReducer**:Redux Router 内置的 reducer 之一,持有当前 `location` 对象。  
- **Router 组件**:在 React 组件树中订阅 `store.location`,根据不同路径渲染不同 Navigator 或 Screen。  

以上流程展示了:从点击分发 action,到中间件拦截,再到 store 更新,最后 React 根据 new state 渲染页面的完整闭环。

---

## 三、安装与基础配置

下面以 `redux-first-router` 为例,在 React Native 项目中集成 Redux Router。

### 3.1 安装依赖

```bash
# 安装核心包
yarn add redux redux-first-router react-redux
# 如果要使用异步中间件(可选)
yarn add redux-thunk
注意redux-first-router 包含了中间件和核心 utils,旧有的 react-router-redux 已不维护,不推荐使用。

3.2 定义路由映射 (routesMap)

在项目中新建 src/routesMap.js,将页面对应关系写入 routesMap:

// src/routesMap.js

// 1. 导入页面组件
import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';
import LoginScreen from './screens/LoginScreen';
import ProfileScreen from './screens/ProfileScreen';

export const routesMap = {
  // action 类型:路由配置对象
  HOME: {
    path: '/home',
    thunk: async (dispatch, getState) => {
      // 可选:页面切换时执行异步逻辑,比如拉取列表数据
      console.log('Navigating to HOME');
    },
  },
  DETAIL: {
    path: '/detail/:id', // 带参数
    thunk: async (dispatch, getState, {history,action}) => {
      // action.payload.id 可获取 id
      console.log('DETAIL id=', action.payload.id);
    },
  },
  LOGIN: {
    path: '/login',
  },
  PROFILE: {
    path: '/profile',
    // 某些页面需要鉴权,可在 before hook 中判断是否登录
    thunk: async (dispatch, getState) => {
      const { auth } = getState();
      if (!auth.loggedIn) {
        dispatch({ type: 'LOGIN_REDIRECT', payload: {} });
      }
    },
  },
  // 处理重定向 action
  LOGIN_REDIRECT: {
    path: '/login',
  },
};

export const HOME = 'HOME';
export const DETAIL = 'DETAIL';
export const LOGIN = 'LOGIN';
export const PROFILE = 'PROFILE';
export const LOGIN_REDIRECT = 'LOGIN_REDIRECT';
  • path 可以带参数,如 :id ,DCDS 即等价于 /:id 形式。
  • thunk:可选,当路由被转发时会执行的异步函数,参数包括 dispatch, getState, extraArgs,可用于做数据预加载、鉴权等。
  • 同一 action type 只能出现一次;若想为某些 action 设置重定向,可单独写一个 LOGIN_REDIRECT

3.3 创建 Store 与 Router

src/store.js 中初始化 Redux Store,将 redux-first-router 集成进来:

// src/store.js
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { connectRoutes } from 'redux-first-router';
import thunk from 'redux-thunk'; // 可选,若需要异步 action

// 1. 导入 routesMap
import { routesMap } from './routesMap';

// 2. 定义你自己的 reducers
import authReducer from './reducers/auth';
import dataReducer from './reducers/data';

// 3. 生成 Router:生成中间件、reducer,和 selector
const {
  reducer: locationReducer,
  middleware: routerMiddleware,
  enhancer: routerEnhancer,
  initialDispatch,
} = connectRoutes(routesMap, {
  // 选项
  initialDispatch: false, // 我们将在 store 创建后手动触发
  querySerializer: (query) => query, // RN 不需要 Qs 序列化
});

// 4. 合并 reducers
const rootReducer = combineReducers({
  location: locationReducer, // Redux Router 内置的 location reducer
  auth: authReducer,
  data: dataReducer,
  // … 其他 reducer
});

// 5. 创建 store,应用 routerEnhancer 与 中间件
const middlewares = [routerMiddleware, thunk];

const enhancers = [applyMiddleware(...middlewares), routerEnhancer];

const store = createStore(rootReducer, compose(...enhancers));

// 6. 手动触发初始 location action
initialDispatch();

export default store;
  • connectRoutes(routesMap, options)

    • 返回一个对象,包含:

      • reducer:路由管理 reducer,命名为 locationReducer,负责存储 { pathname, type, payload, query }
      • middleware:监听所有发往 store 的 action,将匹配到路由的 action 转发。
      • enhancer:用于拓展 store,处理一些路由初始化的逻辑。
      • initialDispatch():初始触发将当前 location 录入 store,使 React 初次渲染时已经有正确 state。
  • initialDispatch: false

    • 禁用自动初始化,手动在创建 store 后调用 initialDispatch(),确保 store 已经设完所有 middleware 再执行初始路由分发。

四、在 React Native 中渲染导航(Router 组件)

接下来我们需要在应用最外层创建一个 Router 组件,监听 store.location,根据不同的 location.pathname 渲染对应的 Navigator 或 Screen。以下示例使用官方的 @react-navigation/native 配合 Redux Router,但你也可选择原生 NavigatorIOSreact-native-screens 等替代方案。

4.1 安装 React Navigation 依赖(可选)

yarn add @react-navigation/native @react-navigation/stack
# 然后安装依赖库
yarn add react-native-safe-area-context react-native-screens

4.2 创建自定义 Router

// src/Router.js
import React from 'react';
import { useSelector } from 'react-redux';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';
import LoginScreen from './screens/LoginScreen';
import ProfileScreen from './screens/ProfileScreen';

const Stack = createStackNavigator();

export default function Router() {
  // 1. 从 Redux store 中获取 location 信息
  const location = useSelector((state) => state.location);
  // location 结构示例:
  // {
  //   pathname: '/home',
  //   type: 'HOME',
  //   payload: {},
  //   query: {},
  // }

  // 2. 确定初始路由名(去掉前导斜杠)
  const routeName = location.pathname.replace(/^\//, '').toUpperCase() || 'HOME';

  // 3. 根据 routeName 决定当前要渲染哪个页面
  //    也可以进一步处理 payload、query 作为 params 透传
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{ headerShown: true }}
      >
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
        <Stack.Screen name="Login" component={LoginScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
      </Stack.Navigator>

      {/* 4. 同步 Redux Router 状态到 React Navigation */}
      {/*    这里我们不使用导航库的 state 管理,而是根据 location 实现“单向绑定” */}
      <StackScreensBind
        routeName={routeName}
        payload={location.payload}
      />
    </NavigationContainer>
  );
}

// 5. 实现一个“绑定组件”,监听 routeName 变更后触发页面跳转
import { useEffect, useRef } from 'react';
import { CommonActions, useNavigationContainerRef } from '@react-navigation/native';

function StackScreensBind({ routeName, payload }) {
  const navigationRef = useNavigationContainerRef();
  const prevRouteName = useRef('');

  useEffect(() => {
    if (!navigationRef.isReady()) return;
    if (routeName !== prevRouteName.current) {
      // 根据 routeName 分发对应的 Navigation Action
      switch (routeName) {
        case 'HOME':
          navigationRef.dispatch(
            CommonActions.navigate({ name: 'Home' })
          );
          break;
        case 'DETAIL':
          navigationRef.dispatch(
            CommonActions.navigate({
              name: 'Detail',
              params: { id: payload.id },
            })
          );
          break;
        case 'LOGIN':
          navigationRef.dispatch(
            CommonActions.navigate({ name: 'Login' })
          );
          break;
        case 'PROFILE':
          navigationRef.dispatch(
            CommonActions.navigate({ name: 'Profile' })
          );
          break;
        // … 其他 case
        default:
          navigationRef.dispatch(
            CommonActions.navigate({ name: 'Home' })
          );
      }
      prevRouteName.current = routeName;
    }
  }, [routeName, payload, navigationRef]);

  return null;
}

4.2.1 说明

  1. useSelector(state => state.location)

    • 从 Redux Store 中读取当前路由状态 location,包含 pathnamepayloadquery 等。
    • routeNamepathname 派生,如 /detail/42 对应 routeName = 'DETAIL'
  2. StackScreensBind

    • 使用 React Navigation 提供的 navigationRef,通过 CommonActions.navigate 将路由跳转动作传给 Navigator。
    • 只要 routeName 变化,就会执行一次 dispatch,实现单向绑定:Redux State → React Navigation。
    • 注意:不要在这里直接维护 navigation 对象(如 useNavigation),要使用 navigationRef 来确保在 NavigationContainer 外可用。
  3. 初始渲染

    • initialRouteName="Home" 只会在首次加载时使用。之后所有跳转都走 StackScreensBind,与 location 同步。
  4. payload 透传

    • 对于带参数的路由(如 DETAIL),我们通过 params: { id: payload.id } 的方式,将 Redux 中的参数传给页面组件。

五、页面组件示例与跳转逻辑

为了完整呈现「Redux Router + React Native Navigator」的配合使用,这里给出几个页面组件示例,并演示如何触发路由跳转。

5.1 HomeScreen.js(主列表页)

// src/screens/HomeScreen.js
import React from 'react';
import { View, Text, Button, FlatList, StyleSheet } from 'react-native';
import { useDispatch } from 'react-redux';
import { DETAIL } from '../routesMap';

const sampleData = [
  { id: '1', title: 'Item 1' },
  { id: '2', title: 'Item 2' },
  { id: '3', title: 'Item 3' },
];

export default function HomeScreen() {
  const dispatch = useDispatch();

  const goToDetail = (id) => {
    dispatch({
      type: DETAIL,
      payload: { id },
    });
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home Screen 列表</Text>
      <FlatList
        data={sampleData}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.item}>
            <Text>{item.title}</Text>
            <Button
              title="详情"
              onPress={() => goToDetail(item.id)}
            />
          </View>
        )}
      />
      <View style={styles.footer}>
        <Button
          title="个人中心"
          onPress={() => dispatch({ type: 'PROFILE', payload: {} })}
        />
        <Button
          title="登出"
          onPress={() => {
            // 假设 dispatch 触发登出后跳转到登录
            dispatch({ type: 'LOGOUT', payload: {} });
            dispatch({ type: 'LOGIN', payload: {} });
          }}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 12 },
  item: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 12,
    borderBottomColor: '#DDD',
    borderBottomWidth: 1,
  },
  footer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginTop: 20,
  },
});
  • goToDetail(id) 中,通过 dispatch({ type: DETAIL, payload: { id } }) 发出导航 action,触发 Redux Router 中间件,将路由状态更新为 /detail/:id,最终通过 StackScreensBind 导航到 DetailScreen。

5.2 DetailScreen.js(详情页)

// src/screens/DetailScreen.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { HOME } from '../routesMap';

export default function DetailScreen() {
  const dispatch = useDispatch();
  // 从 Redux location.payload 中拿到 id
  const { payload } = useSelector((state) => state.location);
  const { id } = payload;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Detail Screen</Text>
      <Text style={styles.content}>展示 Item { id } 的详情内容...</Text>
      <Button
        title="返回列表"
        onPress={() => dispatch({ type: HOME, payload: {} })}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 12 },
  content: { fontSize: 16, marginBottom: 20 },
});
  • 通过 useSelector(state => state.location.payload) 获取 id,然后在 UI 中展示。
  • “返回列表”按钮直接 dispatch { type: HOME },将路由切回 /home

5.3 LoginScreen.js(登录页)

// src/screens/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, Button, TextInput, StyleSheet } from 'react-native';
import { useDispatch } from 'react-redux';
import { HOME } from '../routesMap';

export default function LoginScreen() {
  const dispatch = useDispatch();
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = () => {
    // 真实场景应调用后台 API 验证
    if (username === 'user' && password === '1234') {
      dispatch({ type: 'LOGIN_SUCCESS', payload: { username } });
      dispatch({ type: HOME, payload: {} }); // 登录成功后回到首页
    } else {
      alert('登录失败');
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Login Screen</Text>
      <TextInput
        style={styles.input}
        placeholder="用户名"
        onChangeText={setUsername}
        value={username}
      />
      <TextInput
        style={styles.input}
        placeholder="密码"
        secureTextEntry
        onChangeText={setPassword}
        value={password}
      />
      <Button title="登录" onPress={handleLogin} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, justifyContent: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24, textAlign: 'center' },
  input: {
    height: 48,
    borderColor: '#CCC',
    borderWidth: 1,
    borderRadius: 4,
    marginBottom: 12,
    paddingHorizontal: 8,
  },
});
  • 将登录结果保存在 auth reducer 中,登录成功后 dispatch HOME 路由,回到首页。

5.4 ProfileScreen.js(个人中心页,需鉴权)

// src/screens/ProfileScreen.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { HOME } from '../routesMap';

export default function ProfileScreen() {
  const dispatch = useDispatch();
  const { auth } = useSelector((state) => state);

  // 如果未登录,可跳转到登录页
  if (!auth.loggedIn) {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>请先登录才能查看个人中心</Text>
        <Button
          title="去登录"
          onPress={() => dispatch({ type: 'LOGIN', payload: {} })}
        />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Profile Screen</Text>
      <Text style={styles.content}>欢迎,{auth.username}!</Text>
      <Button
        title="退出登录"
        onPress={() => {
          dispatch({ type: 'LOGOUT', payload: {} });
          dispatch({ type: HOME, payload: {} });
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 12 },
  content: { fontSize: 16, marginBottom: 20 },
  text: { fontSize: 16, marginBottom: 20 },
});
  • PROFILE 路由对应此页面,routesMap 中已在 thunk 里检查了 auth.loggedIn,如果未登录,会 dispatch LOGIN_REDIRECT,最终路由跳转到 LoginScreen
  • 在组件内再做一次 safeguard,保证不会在未登录状态下渲染个人信息。

六、状态管理与 Reducer 示例

为了完整演示,下面给出 authReducerdataReducer 的简单实现示例。

6.1 authReducer.js

// src/reducers/auth.js
const INITIAL_STATE = {
  loggedIn: false,
  username: null,
};

export default function authReducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
      return {
        ...state,
        loggedIn: true,
        username: action.payload.username,
      };
    case 'LOGOUT':
      return INITIAL_STATE;
    default:
      return state;
  }
}
  • LOGIN_SUCCESS 会在 LoginScreen 成功登录后 dispatch,用于保存用户名、设置登录状态。
  • LOGOUT 清空用户信息。

6.2 dataReducer.js

// src/reducers/data.js
const INITIAL_STATE = {
  items: ['示例 A', '示例 B', '示例 C'],
};

export default function dataReducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload.item],
      };
    // 其他数据相关 action
    default:
      return state;
  }
}
  • 通常在 HomeScreen 可以 dispatch 对应 action,动态更新列表等。

七、完整项目文件结构示意

MyApp/
├── App.js                   ← 根组件,包裹 Provider 与 Router
├── package.json
├── src/
│   ├── routesMap.js         ← 定义 routesMap 与常量
│   ├── store.js             ← 创建 Redux Store
│   ├── reducers/
│   │   ├── auth.js
│   │   └── data.js
│   ├── Router.js            ← 根据 location 渲染 NavigationContainer
│   └── screens/
│       ├── HomeScreen.js
│       ├── DetailScreen.js
│       ├── LoginScreen.js
│       └── ProfileScreen.js
└── ...
  • App.js

    import React from 'react';
    import { Provider } from 'react-redux';
    import store from './src/store';
    import Router from './src/Router';
    
    export default function App() {
      return (
        <Provider store={store}>
          <Router />
        </Provider>
      );
    }

八、图解:Redux Router + React Native Navigation 流程

┌────────────────────────┐
│   用户点击按钮 (e.g. “详情”)  │
└────────────────────────┘
           │ dispatch({ type: 'DETAIL', payload: { id: '1' } })
           ▼
┌────────────────────────┐
│  Redux 首次 dispatch    │
└────────────────────────┘
           │
           ▼
┌────────────────────────┐
│ connectRoutes 中间件   │
│   • 匹配 ACTION.TYPE    │
│   • 生成新 location     │
│   • dispatch LOCATION   │
└────────────────────────┘
           │
           ▼
┌────────────────────────┐
│  Redux Reducer 更新     │
│   state.location = {    │
│     pathname: '/detail/1', │
│     type: 'DETAIL',       │
│     payload: { id: '1' }  │
│   }                     │
└────────────────────────┘
           │
           ▼
┌────────────────────────┐
│ React-Redux mapStateToProps │
│   组件 Router 读取 state.location
└────────────────────────┘
           │
           ▼
┌────────────────────────┐
│ StackScreensBind 组件   │
│   • location.pathname  │
│   • 导航到 DetailScreen  │
│   • 并传递 params: { id: '1' } │
└────────────────────────┘
           │
           ▼
┌────────────────────────┐
│ DetailScreen 渲染       │
│   • 获取 params.id = '1' │
│   • 显示详情页面         │
└────────────────────────┘
  • 以上流程展示了“Dispatch → Middleware → Reducer → React Binding → Navigation” 的完整闭环。
  • 任何路由跳转都遵循此过程,且可在 thunk 中预先处理异步和鉴权。

九、常见问题与优化建议

  1. 如何处理页面回退?

    • 如果在 DetailScreen 中按设备的“后退”按钮(Android BackHandler),需要调用 navigation.goBack()。你也可以 dispatch { type: 'HOME' },直接将 store 的 location 切回 /home
    • 如果需要更细粒度控制返回行为,可结合 React Navigation 的 useBackHandler Hook,在 backHandler 中 dispatch Redux 路由 action。
  2. 路由切换卡顿

    • 如果页面组件较复杂(大量图片、列表、地图等),切换时可能出现短暂卡顿。可考虑:

      • thunk 中预先加载数据,等数据就绪后再跳转。
      • 使用 React Navigation 的 Suspenselazy 加载组件,动态按需渲染。
  3. 路由权限与鉴权

    • routesMap 中为需要鉴权的路由添加 thunk,在其中检查 getState().auth.loggedIn,如未登录可 dispatch LOGIN_REDIRECT 或直接跳转。
    • 例如,当用户 dispatch { type: 'PROFILE' } 时,routesMap.PROFILE.thunk 会先执行鉴权逻辑。
  4. 支持深度链接(Deep Link)

    • React Native 支持通过 Linking 监听外部 URL。在应用启动时,可调用 initialDispatch() 并结合 history 选项,让 redux-first-router 根据 Linking.getInitialURL() 解析深度链接并跳转到对应页面。
  5. 性能监控与日志

    • 推荐在开发环境集成 Redux DevTools,通过 redux-first-router 的 middleware 记录路由 action,实时可视化导航流。
    • 在生产环境中可通过 thunk 日志或自定义 Logger 中间件,采集用户在 App 内的跳转轨迹,用于数据分析。

十、总结

本文介绍了使用 Redux Router(redux-first-router) 在 React Native 中构建现代化导航方案的方法与实践。从核心概念、安装配置、Store 搭建、Router 组件实现,到页面组件代码示例,再到状态管理与常见问题解决,全面覆盖了从零到一的全过程:

  1. 核心优势:将导航状态纳入 Redux,实现“状态可回溯、可调试、可中间件拦截”的一体化管理。
  2. routesMap 配置:在一个地方定义所有路由,清晰明了;支持 path 参数、thunk 逻辑、鉴权等。
  3. Store & MiddlewareconnectRoutes 生成 locationReducerrouterMiddlewarerouterEnhancer,并通过 initialDispatch() 初始化。
  4. Router 组件:使用 useSelector 读取 state.location,通过 StackScreensBind 与 React Navigation 同步状态,完成页面跳转。
  5. 页面跳转示例:各个 screen 通过 dispatch({ type: ROUTE_TYPE, payload }) 触发跳转,实现单向数据流。
  6. 状态管理与鉴权:在 reducer 中处理鉴权状态,在 routesMap.thunk 中检查登录态并重定向到登录页。
  7. 扩展场景:深度链接、BackHandler 处理、性能优化、日志采集等最佳实践。

通过将导航与 Redux 深度耦合,你可以获得更强的可控性与可维护性,尤其适合需要复杂页面流转、权限控制、统计分析的大型项目。希望这篇详解能帮助你快速掌握 Redux Router 在 React Native 中的使用,打造出更为现代化、可维护的移动应用。

React Native 中,设备方向(横屏/竖屏)切换往往会影响布局与用户体验。借助开源社区的 Orientation(或更常用的 “react-native-orientation-locker”) 库,我们可以轻松检测、锁定、解锁和响应方向变化。本文以“React Native 必备神器:Orientation,轻松搞定设备方向管理”为题,详细介绍安装、API、示例代码与实战场景,并配合图解,帮助你快速上手。


一、为什么需要方向管理

  1. 不同页面对横竖屏要求不同

    • 视频播放器、游戏等通常需要 横屏
    • 新闻详情、文章列表常用 竖屏
    • 一个应用中多个界面流畅切换时,需要动态锁定或解锁方向。
  2. 响应式布局优化

    • 当用户从竖屏切换到横屏时,布局需要重新计算(如两列变三列、图片宽度拉满屏幕等);
    • 如果不关注方向变化,UI 会出现重叠、撑破屏幕、拉伸失真等问题。
  3. 导航栈与方向冲突

    • React Navigation 等导航库本身并不直接管理设备方向,需要手动结合方向锁定逻辑;
    • 如果切换页面时忘记解除锁定,可能导致用户无法切换回默认方向。

思考题:如果你在一个横屏游戏界面深度点击“返回”到一个竖屏列表页,但页面却依然保持横屏,这会严重影响用户体验——这是因为没有正确“解锁”方向。
理想流程:

进入游戏界面 → 锁定为横屏  
用户点击返回 → 自动切换回竖屏  
再次进入其他界面 → 根据需求决定是否横屏或竖屏  

二、库选型与安装

目前社区中常用来管理方向的库有两个:

  1. react-native-orientation(早期)
  2. react-native-orientation-locker(维护更活跃,支持更多新特性)

本文以 react-native-orientation-locker 为主;若你坚持使用原版 react-native-orientation,API 基本一致,只需替换包名即可。

2.1 安装

1. 使用 Yarn

yarn add react-native-orientation-locker

2. 使用 npm

npm install react-native-orientation-locker --save

3. iOS 原生配置(RN 0.60+ Autolinking 自动链接)

  • 进入 iOS 目录cd ios && pod install && cd ..
  • 打开 Xcode → 目标项目 → Info.plist → 添加允许的方向配置(详见下文)。
注意:如果你的项目原本只勾选了 Portrait,但后续想支持 Landscape,就必须在 Info.plist 中将对应方向打开,否则锁定逻辑无法生效。

三、Info.plist 与 AndroidManifest.xml 配置

3.1 iOS (Info.plist)

在 Xcode 中选择项目 Target → “General”Deployment InfoDevice Orientation,勾选需要支持的方向(如下图所示):

[✔︎] Portrait
[✔︎] Upside Down          ← (通常 iPhone 不需要)
[✔︎] Landscape Left
[✔︎] Landscape Right

图解:Info.plist 中的方向选项

--------------------------------
| Deployment Info             |
| --------------------------- |
| Device Orientation          |
| [✓] Portrait                |
| [ ] Upside Down             |
| [✓] Landscape Left          |
| [✓] Landscape Right         |
--------------------------------

若你只想在某些页面支持横屏,其他页面只竖屏,也需要确保这里至少勾选了所有可能的方向;后续通过代码动态 Lock(锁定)/Unlock(解锁) 达到切换效果。若这里只勾选了 Portrait,则无论如何锁定方向,应用也无法切换到 Landscape。

3.2 Android (AndroidManifest.xml)

默认情况下,Android 已支持横竖屏,只需在特定 Activity 里手动修改 android:screenOrientation。不过使用 react-native-orientation-locker 时,一般不需手动改动 AndroidManifest.xml,库内部会帮助动态切换。
但如果你想全局默认只支持竖屏,在 AndroidManifest.xml 中可以在 <activity> 标签里添加:

<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    ...
>

这样主 Activity 会锁定竖屏;当在代码中调用 Orientation.lockToLandscape() 时,会动态解除并切换到横屏。只要 screenOrientation="fullSensor"unspecified,库才能自由切换;若你将其设为固定值,会导致库无效。因此推荐保留默认 unspecified,或在需要时进行局部控制,再用代码锁定。


四、基础用法与 API 详解

安装完成后,我们在项目中导入 react-native-orientation-locker,并结合 React Native 组件或 Hook 来使用。下面先罗列常用 API,再结合示例代码演示。

4.1 常用 API

import Orientation, {
  PORTRAIT,
  LANDSCAPE,
  LANDSCAPE_LEFT,
  LANDSCAPE_RIGHT,
  DEFAULT,
  useDeviceOrientation,
  useLockOrientation,
} from 'react-native-orientation-locker';

1. 锁定方向

  • Orientation.lockToPortrait() → 锁定为竖屏(竖直方向)
  • Orientation.lockToLandscape() → 锁定为横屏(自动选择左右,跟随传感器)
  • Orientation.lockToLandscapeLeft() → 强制向左横屏
  • Orientation.lockToLandscapeRight() → 强制向右横屏
  • Orientation.unlockAllOrientations() → 解除锁定,允许随系统方向旋转

2. 获取当前方向

  • Orientation.getDeviceOrientation(callback)

    • 回调 callback(orientation)orientation 为字符串,可能值:

      • 'PORTRAIT''LANDSCAPE-LEFT''LANDSCAPE-RIGHT''PORTRAIT-UPSIDEDOWN''UNKNOWN'

3. 监听方向变化

  • Orientation.addDeviceOrientationListener(callback)

    • 当设备方向改变时触发回调,参数同上;
  • Orientation.removeDeviceOrientationListener(callback)

    • 移除监听,参数为之前注册的 callback 函数。

4. Hook 封装(函数组件推荐)

  • const deviceOrientation = useDeviceOrientation();

    • 返回一个对象 { portrait, landscape, portraitUpsideDown, lock, ... } 等布尔值,表示当前方向。
  • const lockOrientation = useLockOrientation();

    • 返回一个函数 lockOrientation(orientationString),可传入 'PORTRAIT' | 'LANDSCAPE-LEFT' | 'LANDSCAPE-RIGHT' | 'DEFAULT' 等。

4.2 完整示例:监测 + 锁定 + 解锁

下面示例会在页面顶部显示当前方向(文字提示),下方有四个按钮,分别演示锁定竖屏、锁定横屏、解除锁定、获取当前方向。

// OrientationDemo.js
import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  Button,
  StyleSheet,
  SafeAreaView,
  Platform,
} from 'react-native';
import Orientation, {
  useDeviceOrientation,
} from 'react-native-orientation-locker';

export default function OrientationDemo() {
  // 1. 使用 Hook 获取当前设备方向状态
  const deviceOrientation = useDeviceOrientation();
  // deviceOrientation 对象结构示例:
  // {
  //   orientation: 'PORTRAIT' | 'LANDSCAPE-LEFT' | ...,
  //   portrait: true|false,
  //   landscape: true|false,
  //   portraitUpsideDown: true|false,
  //   lock: true|false, // 是否已被手动锁定
  // }

  // 2. 本地 state 保存文字提示
  const [current, setCurrent] = useState('UNKNOWN');

  useEffect(() => {
    // 当 deviceOrientation.orientation 改变时,更新文字
    setCurrent(deviceOrientation.orientation);
  }, [deviceOrientation.orientation]);

  // 3. 按钮处理函数
  const lockPortrait = () => {
    Orientation.lockToPortrait();
  };
  const lockLandscape = () => {
    Orientation.lockToLandscape();
  };
  const unlock = () => {
    Orientation.unlockAllOrientations();
  };
  const showCurrent = () => {
    Orientation.getDeviceOrientation(ori => {
      alert('当前方向:' + ori);
    });
  };

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.title}>React Native 方向管理示例</Text>
      <Text style={styles.info}>
        当前方向:{current} {'\n'}
        锁定状态:{deviceOrientation.lock ? '已锁定' : '未锁定'}
      </Text>

      <View style={styles.buttonRow}>
        <Button title="锁定竖屏" onPress={lockPortrait} />
        <Button title="锁定横屏" onPress={lockLandscape} />
      </View>
      <View style={styles.buttonRow}>
        <Button title="解除锁定" onPress={unlock} />
        <Button title="显示当前方向" onPress={showCurrent} />
      </View>

      <Text style={styles.hint}>
        ※ 在 iOS 模拟器中,⌘+←/→ 可以手动切换方向;Android 模拟器顶部虚拟按键也可切换。
      </Text>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#FFF',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  info: {
    fontSize: 16,
    marginBottom: 24,
  },
  buttonRow: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 16,
  },
  hint: {
    marginTop: 32,
    fontSize: 12,
    color: '#666',
  },
});

4.2.1 关键点说明

  1. useDeviceOrientation Hook

    • 自动监听方向变化并返回一个对象,包含:

      • orientation(字符串)
      • portraitlandscapeportraitUpsideDown(布尔)
      • lock(是否锁定)
    • 使用 orientation 作为文字提示显示给用户。
  2. Orientation.lockToPortrait() / Orientation.lockToLandscape()

    • 即时锁定当前页面为竖屏或横屏;无需重启页面。
  3. Orientation.unlockAllOrientations()

    • 解除锁定,允许页面随系统方向变化(如手机旋转或模拟器快捷键切换)。
  4. Orientation.getDeviceOrientation(callback)

    • 弹出 Alert 或用于日志,回调返回当前方向字符串。
  5. Platform 差异

    • 在 iOS 模拟器,按 ⌘ + ←/→ 可切换横竖屏;
    • 在 Android 模拟器,可点击顶部按钮或执行快捷键 Ctrl + F11/F12

五、图解:方向监听与切换流程

为了帮助理解,下面用一张简化的流程图说明“方向变化监听与锁定/解锁”机制。

┌────────────────────────────────────────────────────────────────┐
│                        启动 App / 进入页面                     │
└────────────────────────────────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────────┐
│ useDeviceOrientation Hook 启动,自动调用 addListener =>      │
│ 倾听 deviceOrientation.orientation,保留在内存中              │
└────────────────────────────────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────────┐
│ 普通情况下:用户旋转设备 / 模拟器快捷键                         │
│ 触发系统方向变化事件                                             │
│   └─ Native 层捕获新方向                                         │
│   └─ 通过 NativeModule 通知 JS 层                                 │
│   └─ useDeviceOrientation 更新 orientation、portrait 等字段        │
│   └─ React 重新渲染并更新 UI                                      │
└────────────────────────────────────────────────────────────────┘
                     │
      ┌──────────────┴───────────────┐
      │                              │
      ▼                              ▼
┌───────────────┐            ┌───────────────┐
│ 锁定方向      │            │ 未锁定方向    │
│ (lockToXXX)   │            │ (unlockAll)   │
│               │            │               │
│ JS 调用        │            │ JS 调用        │
│ Orientation   │            │ Orientation   │
│ lockToXXXX()  │            │ unlockAll()   │
└───────────────┘            └───────────────┘
      │                              │
      │                              │
      │                              │
      │      ┌────────────────────────────────────────┐
      │       │ 解锁后可随系统方向变化(与上方“普通情况”一致)     │
      │       └────────────────────────────────────────┘
      │
      ▼
┌────────────────────────────────────────────────────────────────┐
│ 锁定后:Native 层立即强制切换到对应方向  (如 Portrait/Landscape) │
│   └─ JS 层收到一次 orientation 更新                             │
│   └─ React 渲染基于锁定方向的布局                                 │
│   └─ 之后系统旋转输入将被忽略  (continue to lock)                │
└────────────────────────────────────────────────────────────────┘

图解说明

  1. 未锁定状态:任何系统方向变化(旋转、快捷键)都会通知 JS,并更新 UI;
  2. 锁定状态:一旦调用 lockToPortrait()lockToLandscape(),JS 会调用 Native 将设备方向强制切换到指定方向,并忽略后续系统旋转事件
  3. 解锁后:再一次调用 unlockAllOrientations() 后,恢复对系统旋转的正常监听。

六、实战场景:横屏视频播放 & 列表竖屏切换

下面给出一个典型的实战场景:在一个页面中有竖屏列表,点击某个条目后跳转到横屏视频播放页,播放完成或点击“返回”后,自动切换回竖屏列表。

6.1 目录结构示例

src/
├─ screens/
│   ├─ VideoListScreen.js        ← 竖屏列表页
│   └─ VideoPlayerScreen.js      ← 横屏播放页
└─ App.js                        ← 根导航

6.2 VideoListScreen.js(竖屏模式)

// VideoListScreen.js
import React, { useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import Orientation from 'react-native-orientation-locker';

export default function VideoListScreen({ navigation }) {
  useEffect(() => {
    // 进入列表页,锁定竖屏
    Orientation.lockToPortrait();
    return () => {
      // 可选:离开页面时解除锁定
      // Orientation.unlockAllOrientations();
    };
  }, []);

  const videos = [
    { id: '1', title: 'React Native 入门教程' },
    { id: '2', title: 'Animated 深度解析' },
    { id: '3', title: 'RN 性能优化技巧' },
  ];

  return (
    <View style={styles.container}>
      <Text style={styles.title}>视频列表(竖屏)</Text>
      <FlatList
        data={videos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity
            style={styles.item}
            onPress={() => navigation.navigate('VideoPlayer', { videoId: item.id })}
          >
            <Text style={styles.itemText}>{item.title}</Text>
          </TouchableOpacity>
        )}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, backgroundColor: '#FAFAFA' },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 12 },
  item: {
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#DDD',
  },
  itemText: { fontSize: 16 },
});

6.2.1 说明

  • useEffect 中调用 Orientation.lockToPortrait(),确保列表页始终保持竖屏
  • 点击列表项导航到播放页时,不需要立即解除锁定;由播放页决定。
  • 如果你希望在离开列表页时解除锁定,也可以在 return 回调里调用 Orientation.unlockAllOrientations(),但注意如果同时在播放页也调用,会产生重复调用。

6.3 VideoPlayerScreen.js(横屏模式)

// VideoPlayerScreen.js
import React, { useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
import Orientation from 'react-native-orientation-locker';

export default function VideoPlayerScreen({ route, navigation }) {
  useEffect(() => {
    // 进入播放页时,锁定横屏
    if (Platform.OS === 'ios') {
      Orientation.lockToLandscapeLeft(); // iOS 建议使用具体方向
    } else {
      Orientation.lockToLandscape(); // Android 可直接锁横屏
    }

    return () => {
      // 离开播放页时,切换回竖屏
      Orientation.lockToPortrait();
    };
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.text}>正在播放视频 ID: {route.params.videoId}</Text>
      {/* 这里通常放 Video 组件,全屏播放 */}
      <TouchableOpacity
        style={styles.backBtn}
        onPress={() => navigation.goBack()}
      >
        <Text style={styles.backText}>退出播放</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: { color: '#FFF', fontSize: 18, marginBottom: 20 },
  backBtn: {
    position: 'absolute',
    top: 40,
    left: 20,
    padding: 8,
    backgroundColor: '#FFF',
    borderRadius: 4,
  },
  backText: { color: '#000', fontSize: 14 },
});

6.3.1 说明

  1. 横屏锁定

    • 在 iOS 上建议调用 Orientation.lockToLandscapeLeft()Orientation.lockToLandscapeRight(),这样横屏方向更可控;
    • 在 Android 上直接调用 Orientation.lockToLandscape() 即可;
  2. 播放完成或点击返回

    • 当用户导航回列表页时(navigation.goBack()),useEffect 的清理函数自动执行 Orientation.lockToPortrait()
    • 列表页会再次锁定竖屏或遵循全局默认方向。
  3. 注意返回动画时机

    • 如果你想在页面真正退出之前(监听 beforeRemove)就先锁回竖屏,可在导航钩子里先调用,避免短暂横屏闪烁。

七、进阶场景与常见问题

7.1 在模态弹窗中也要锁定方向

如果你的播放页是以模态形式出现(如 React Navigation 的 Modal),页面依旧可以像普通页面那样调用 Orientation.lockToLandscape()。只要顶部 Activity/UIViewController 置为支持横屏即可,库会生效。

7.2 与 React Navigation 结合:监听焦点事件

如果你使用 React Navigation,可以在页面获得焦点/失去焦点时再调用锁定/解锁,而不必在 useEffect 里写死。例如:

import { useFocusEffect } from '@react-navigation/native';

useFocusEffect(
  React.useCallback(() => {
    // 页面聚焦时锁定横屏
    Orientation.lockToLandscape();

    return () => {
      // 页面失焦时锁定竖屏
      Orientation.lockToPortrait();
    };
  }, [])
);

此时,当页面被隐藏时,不会立即切换方向,只有在下一个页面获得焦点时才会执行清理函数,减少闪烁。

7.3 监听原生方向改变

你还可以通过 Orientation.addDeviceOrientationListener 监听原生方向变化。例如,在一个仪表盘页面,你想根据横竖屏切换动态调整 UI 布局,可这样写:

useEffect(() => {
  const callback = (orientation) => {
    console.log('设备方向变更为:', orientation);
    // 根据 orientation 判断是否 horizontal/vertical,再 setState 或 set 布局
  };
  Orientation.addDeviceOrientationListener(callback);

  return () => {
    Orientation.removeDeviceOrientationListener(callback);
  };
}, []);
  • 当设备从竖屏变横屏时,orientation 会被回调为 'LANDSCAPE-LEFT''LANDSCAPE-RIGHT'
  • 你可以在回调内处理如 this.setState({ isLandscape: true }),然后在渲染中做条件布局(如 Grid → List 切换)。

7.4 获取默认方向 & 解锁边界

  • Orientation.getAutoRotateState(callback)

    • 回调会返回两个布尔:autoRotatelocked,表示系统是否允许自动旋转;
    • 如果 locked===trueautoRotate===false,说明用户在系统设置里关闭了自动旋转,任何代码锁定都无法生效
  • 解锁后默认为哪个方向?

    • 调用 unlockAllOrientations() 后,会恢复系统默认方向(即由系统决定传感器方向)。如果你想保证一定是竖屏,可以直接调用 lockToPortrait() 而不是解锁。

八、小结与最佳实践

  1. 务必在 Info.plist / AndroidManifest.xml 中允许目标方向,否则后续锁定逻辑会失效。
  2. 优先使用 useLockOrientationuseDeviceOrientation 等 Hook,使代码更简洁、可读性更高,并自动在组件卸载时移除监听。
  3. 页面切换时结合 React Navigation 的 useFocusEffect,在获得焦点时锁定方向,失去焦点时解锁或切换,减少闪烁和“错位”体验。
  4. 避免连续反复锁定/解锁,如果同一个页面内多次调用,会导致 UI 重绘开销;建议在一次 useEffectuseFocusEffect 中完成。
  5. 考虑用户系统设置:如果系统设置了“锁定屏幕方向(autoRotate)”,则代码无法改变,因此可在上层提示用户开启自动旋转。
  6. 不同平台差异

    • iOS 上可精确指定 lockToLandscapeLeft()Right
    • Android 上只能 lockToLandscape(),不区分左右。

通过本文,相信你已经掌握了如何在 React Native 中使用 Orientation(或 react-native-orientation-locker)轻松搞定设备方向的监听、锁定与解锁,完成跨页面的横/竖屏切换,让应用在视频播放、游戏、仪表盘、图表分析等场景下,实现更加优雅的体验。