Chapter 02

核心组件与布局

掌握 React Native 的原生组件体系,用 Flexbox 构建任意界面,理解 FlatList 虚拟化列表的工作原理。

RN 组件就是原生 UI

React Native 的 UI 组件与浏览器中的 HTML 元素有本质区别。当你在浏览器里写 <div>,浏览器使用自己的渲染引擎绘制一个盒子。但 React Native 里写 <View>,框架会调用平台原生 API 创建一个真正的原生视图——iOS 上是 UIView,Android 上是 android.view.View

这就是为什么 React Native 的用户体验比 WebView 方案好得多:动画、滚动、手势等都是原生实现,系统的无障碍(Accessibility)、键盘管理、安全区域等也自然支持。

核心组件速查

View
最基础的容器组件,相当于 HTML 的 div。支持 Flexbox 布局、样式、触摸事件。几乎所有其他组件都是基于 View 构建的。
Text
唯一用于显示文字的组件。与 Web 不同,RN 里所有文本必须包裹在 <Text> 中,不能直接在 <View> 里写纯文字。支持嵌套实现富文本样式。
Image
显示图片,支持本地资源(require)和网络图片({uri: '...'})。网络图片需要指定 width 和 height,否则不会显示。
ScrollView
可滚动的容器,会一次性渲染所有子元素。适合内容量少(少于 20 个)的场景。内容过多时会导致内存爆炸和卡顿。
FlatList
虚拟化列表,只渲染当前可见区域附近的元素,超出视口的元素会被回收复用。大列表的唯一正确选择,支持下拉刷新、上拉加载等。
SectionList
带分组标题的虚拟化列表,适合联系人列表、分类展示等场景。数据格式为 [{title, data: [...]}]。
TextInput
文本输入框,受控组件模式(value + onChangeText)。注意 iOS 和 Android 的默认样式差异,通常需要自定义样式统一外观。
TouchableOpacity
点击时降低不透明度给予视觉反馈的可触摸容器。是实现按钮最常用的方案之一。activeOpacity 属性控制按下时的透明度(默认 0.2)。
Pressable
新一代可触摸组件,API 更灵活,支持 pressed 状态样式、hitSlop 扩大点击区域、Android ripple 水波纹效果。推荐新项目使用。
SafeAreaView
自动处理 iPhone 刘海/灵动岛、Android 状态栏的安全区域,确保内容不被遮挡。推荐用 react-native-safe-area-context 的版本,兼容性更好。

Flexbox 布局——RN 的默认值与 Web 不同

React Native 使用 Flexbox 进行布局,与 CSS Flexbox 概念相同,但有几个重要的默认值差异,初学者经常在这里踩坑:

CSS Flexbox 默认值

  • flexDirection: row(横向排列)
  • alignContent: stretch
  • flexShrink: 1
  • position: static

RN Flexbox 默认值

  • flexDirection: column(纵向排列)✦
  • alignContent: flex-start ✦
  • flexShrink: 0 ✦
  • position: relative

最重要的一点:RN 中 flexDirection 默认是 column(垂直方向),而 Web CSS 默认是 row(水平方向)。理解这一点,就能避免大量布局困惑。

此外,RN 中的 flex: 1 非常常用,它让组件占满父容器的剩余空间。要让根容器撑满整个屏幕,只需给容器设置 flex: 1

FlatList 虚拟化原理

FlatList 是 React Native 处理长列表的核心组件。理解它的虚拟化机制,能帮助你写出高性能的列表界面。

FlatList 虚拟化列表渲染原理 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 屏幕可视区域(viewport) ┌────────────────────────┐ │ Item 3 [已渲染] │ ← initialNumToRender 范围 │ Item 4 [已渲染] │ │ Item 5 [已渲染] │ ← 当前可见区域 │ Item 6 [已渲染] │ │ Item 7 [已渲染] │ ← windowSize 预渲染缓冲 └────────────────────────┘ ↕ 用户滚动 Item 1 [已卸载/回收] ← 超出 windowSize 范围被回收 Item 2 [已卸载/回收] ... Item 8 [已渲染/预载] ← 即将进入视口,提前渲染 Item 9 [未渲染] ... Item 1000 [未渲染] ← 远离视口,不占内存 关键参数: • initialNumToRender = 10 首屏渲染数量 • windowSize = 21 以屏幕高度为单位的渲染窗口(前后各10屏) • maxToRenderPerBatch = 10 每批次最多渲染数量 • updateCellsBatchingPeriod = 50ms 批次更新间隔
警告 永远不要在大列表(超过 20 条数据)里用 ScrollView。ScrollView 会一次性渲染所有子元素并保持在内存中,1000 条数据就意味着 1000 个原生视图同时存在,导致严重卡顿和内存溢出崩溃。

完整 FlatList 示例

import {
  FlatList, View, Text, Image,
  RefreshControl, StyleSheet,
  ListRenderItem
} from 'react-native';
import { useState, useCallback } from 'react';

interface Post {
  id: string;
  author: string;
  avatar: string;
  content: string;
  likes: number;
}

const POSTS: Post[] = Array.from({ length: 100 }, (_, i) => ({
  id: String(i),
  author: `用户 ${i + 1}`,
  avatar: `https://i.pravatar.cc/40?img=${i % 70}`,
  content: `这是第 ${i + 1} 条帖子的内容,展示 FlatList 的用法。`,
  likes: Math.floor(Math.random() * 1000),
}));

// 抽离 renderItem 为独立组件,配合 React.memo 避免不必要的重渲染
const PostItem = React.memo(({ item }: { item: Post }) => (
  <View style={styles.item}>
    <Image source={{ uri: item.avatar }} style={styles.avatar} />
    <View style={styles.itemBody}>
      <Text style={styles.author}>{item.author}</Text>
      <Text style={styles.content}>{item.content}</Text>
      <Text style={styles.likes}>♥ {item.likes}</Text>
    </View>
  </View>
));

export default function FeedScreen() {
  const [refreshing, setRefreshing] = useState(false);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    await new Promise(r => setTimeout(r, 1500)); // 模拟网络请求
    setRefreshing(false);
  }, []);

  return (
    <FlatList
      data={POSTS}
      renderItem={({ item }) => <PostItem item={item} />}
      keyExtractor={item => item.id}          // 必须提供,用于 diff 优化
      initialNumToRender={10}               // 首屏渲染数量
      windowSize={5}                         // 渲染窗口(5倍屏高)
      maxToRenderPerBatch={5}                // 每批次渲染数量
      removeClippedSubviews={true}           // 裁剪屏幕外的视图(Android)
      refreshControl={
        <RefreshControl
          refreshing={refreshing}
          onRefresh={handleRefresh}
          tintColor="#38bdf8"
        />
      }
      ListEmptyComponent={
        <Text style={styles.empty}>暂无数据</Text>
      }
    />
  );
}

StyleSheet 最佳实践

StyleSheet.create() 不只是组织代码的方式,它在运行时会将样式对象序列化为 ID 引用,减少跨线程传输的数据量。此外,它还提供完整的 TypeScript 类型推断,能在编写时发现样式属性拼写错误。

最佳实践 避免在 JSX 中写内联样式对象(style={{ color: 'red' }}),每次渲染都会创建新对象,对垃圾回收造成压力。应始终用 StyleSheet.create 在组件外部定义样式,或使用样式数组 style={[styles.base, isActive && styles.active]}

处理键盘遮挡(KeyboardAvoidingView)

import {
  KeyboardAvoidingView, Platform,
  ScrollView, TextInput
} from 'react-native';

export function CommentForm() {
  return (
    // iOS 用 padding,Android 用 height
    <KeyboardAvoidingView
      style={{ flex: 1 }}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <ScrollView>
        {/* 表单内容 */}
      </ScrollView>
      <TextInput
        placeholder="写下你的评论..."
        style={styles.input}
        multiline
      />
    </KeyboardAvoidingView>
  );
}