CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/布局与列表
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

布局与列表

Flexbox 与 FlatList。

30 分钟

布局与列表

这一课你将能做到

  1. 用 Flexbox 在 RN 里实现水平排列、居中、两端对齐、等分等常见布局
  2. 用 FlatList 渲染千级长列表且保持流畅,配齐分隔线、空状态、下拉刷新、上拉加载
  3. 用 SectionList 实现分组列表,并说出 FlatList 虚拟化的原理和优化参数

为什么要学

移动端 90% 的界面就两种:静态布局和列表。Flexbox 管前者,FlatList 管后者。上一课的 ScrollView 只能应付一屏以内的内容;真实 App 的列表动辄几百上千条,不用虚拟化列表就是卡死。这一课是 RN 开发中日常使用频率最高的一课。

人话:ScrollView 像一次性把所有行李搬上车——东西一多车就压垮;FlatList 像摩天轮,只有转到眼前的座舱里坐人,看不见的座舱是空的,所以装一万条数据也照样流畅。

先建立图像

import { FlatList, Text, View, StyleSheet } from 'react-native';

const DATA = Array.from({ length: 1000 }, (_, i) => ({ id: String(i), title: `第 ${i + 1} 条消息` }));

export default function App() {
  return (
    <FlatList
      data={DATA}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={styles.row}>
          <Text style={styles.text}>{item.title}</Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  row: { padding: 16, borderBottomWidth: 1, borderColor: '#eee' },
  text: { fontSize: 16 },
});

渲染结果:1000 行列表滚动丝滑——因为内存里实际只渲染了屏幕上看得见的十几行,滚出去的行被回收了。

跟着做

步骤 1:Flexbox 三剑客解决 80% 布局

RN 默认 flexDirection: 'column'(主轴纵向)。记住三个属性的分工:

  • flexDirection:主轴方向(row 横向 / column 纵向)
  • justifyContent:主轴上的对齐(flex-start / center / space-between / space-around / space-evenly)
  • alignItems:交叉轴上的对齐(flex-start / center / flex-end / stretch)
import { View, Text } from 'react-native';

export default function App() {
  return (
    <View style={{ flex: 1, padding: 16 }}>
      {/* 1. 水平排列 + 两端对齐:列表项「左文字右箭头」 */}
      <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12 }}>
        <Text>用户名</Text>
        <Text style={{ color: '#ccc' }}>›</Text>
      </View>

      {/* 2. 左固定右自适应:头像 + 内容 */}
      <View style={{ flexDirection: 'row', marginTop: 16 }}>
        <View style={{ width: 48, height: 48, borderRadius: 24, backgroundColor: '#ddd' }} />
        <View style={{ flex: 1, marginLeft: 12 }}>
          <Text style={{ fontWeight: 'bold' }}>张小明</Text>
          <Text style={{ color: '#888', marginTop: 4 }}>右侧内容用 flex: 1 自动占满剩余宽度</Text>
        </View>
      </View>

      {/* 3. 三栏等分:每个子元素 flex: 1 */}
      <View style={{ flexDirection: 'row', marginTop: 24 }}>
        {['点赞', '评论', '分享'].map((t) => (
          <View key={t} style={{ flex: 1, alignItems: 'center' }}>
            <Text>{t}</Text>
          </View>
        ))}
      </View>
    </View>
  );
}

步骤 2:绝对定位与 zIndex

// 右上角小红点
<View style={{ width: 48, height: 48, borderRadius: 24, backgroundColor: '#ddd' }}>
  <View style={{
    position: 'absolute', top: 0, right: 0,
    width: 14, height: 14, borderRadius: 7, backgroundColor: 'red',
  }} />
</View>

RN 只有 relative(默认)和 absolute 两种定位,没有 fixed。absolute 相对最近的非 static 父容器定位。

步骤 3:FlatList 的三个必填属性

  • data:数组数据源
  • renderItem:每行怎么渲染,参数是 { item, index }
  • keyExtractor:返回每行唯一 key(字符串)
<FlatList
  data={list}
  keyExtractor={(item) => item.id}
  renderItem={({ item, index }) => (
    <Text>{index}. {item.title}</Text>
  )}
/>

步骤 4:加分隔线、头部、尾部、空状态

<FlatList
  data={list}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}
  ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: '#eee' }} />}
  ListHeaderComponent={<Text style={{ padding: 16, fontSize: 18, fontWeight: 'bold' }}>全部消息</Text>}
  ListFooterComponent={<Text style={{ padding: 16, textAlign: 'center', color: '#999' }}>没有更多了</Text>}
  ListEmptyComponent={<Text style={{ marginTop: 80, textAlign: 'center', color: '#999' }}>暂无数据</Text>}
/>

想整个页面(头部 banner + 列表)一起滚动?不要用 ScrollView 套 FlatList,把 banner 塞进 ListHeaderComponent 即可,虚拟化依然生效。

步骤 5:下拉刷新

import { useState, useCallback } from 'react';
import { FlatList, RefreshControl } from 'react-native';

const [refreshing, setRefreshing] = useState(false);

const onRefresh = useCallback(async () => {
  setRefreshing(true);
  try {
    await fetchList();        // 重新请求第一页数据
  } finally {
    setRefreshing(false);     // 成功失败都要复位,否则转圈不消失
  }
}, []);

<FlatList
  data={list}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
/>

refreshing 为 true 时,列表顶部自动出现系统原生的下拉转圈动画。注意新版推荐用 refreshControl 属性传 RefreshControl,比直接传 refreshing + onRefresh 可控性更强。

步骤 6:上拉加载更多(分页)

const [list, setList] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loadingRef = useRef(false);   // 防重复触发的守卫

const loadMore = async () => {
  if (!hasMore || loadingRef.current) return;
  loadingRef.current = true;
  try {
    const newData = await fetchList(page + 1);
    if (newData.length === 0) {
      setHasMore(false);
    } else {
      setList((prev) => [...prev, ...newData]);
      setPage((p) => p + 1);
    }
  } finally {
    loadingRef.current = false;
  }
};

<FlatList
  data={list}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  onEndReached={loadMore}
  onEndReachedThreshold={0.3}    // 距底部还有 30% 可视高度时触发
  ListFooterComponent={hasMore ? <Text style={{ textAlign: 'center', padding: 12 }}>加载中…</Text> : <Text style={{ textAlign: 'center', padding: 12, color: '#999' }}>没有更多了</Text>}
/>

onEndReached 在滚动接近底部时触发,但它可能连续触发多次,必须加 loading 守卫。用 ref 而不是 state 做守卫,因为 ref 的修改是同步的,不会等下一次渲染。

步骤 7:SectionList 分组列表

通讯录、按日期分组的账单,用这个:

import { SectionList, Text, View } from 'react-native';

const sections = [
  { title: '今天', data: ['外卖 ¥32', '打车 ¥18'] },
  { title: '昨天', data: ['超市 ¥126', '咖啡 ¥15', '电影 ¥45'] },
];

export default function App() {
  return (
    <SectionList
      sections={sections}
      keyExtractor={(item, index) => item + index}
      renderItem={({ item }) => (
        <View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
          <Text>{item}</Text>
        </View>
      )}
      renderSectionHeader={({ section }) => (
        <Text style={{ backgroundColor: '#f5f5f5', paddingHorizontal: 16, paddingVertical: 8, fontWeight: 'bold' }}>
          {section.title}
        </Text>
      )}
      stickySectionHeadersEnabled   // iOS 上分组头吸顶
    />
  );
}

数据结构是 { title, data: [] } 的数组,renderSectionHeader 渲染每个分组的头部。

讲清楚要点

  1. FlatList 是虚拟化列表:只渲染可视区附近一个「窗口」内的行,滚出去的行被回收,这是流畅的根本原因。窗口大小由 windowSize(单位:屏)控制。
  2. renderItem 尽量轻:行组件复杂时用 React.memo 包一层;不要在 renderItem 里做重计算,数据在进入列表前处理好。
  3. keyExtractor 必须稳定唯一:用数据的真实 id,别用 index——用 index 时删除一行,后面所有行的 key 全变,RN 会销毁重建整个列表,还可能错位。
  4. onEndReached 会重复触发:配合 hasMore + loading 守卫做防重,且用 ref 做守卫(同步生效)。
  5. 嵌套滚动是大坑:ScrollView 套 FlatList 会让虚拟化失效(FlatList 拿到无限高度,一次性渲染全部)。正确做法:头部内容放 ListHeaderComponent,或在 ScrollView 里用 map(仅当数据量确定很小时)。
  6. 行高固定就给 getItemLayout:getItemLayout={(data, index) => ({ length: 60, offset: 60 * index, index })},FlatList 不用测量就能算出滚动位置,scrollToIndex 不闪、性能更好。

你可能会懵的地方

  • 为什么我的 FlatList 根本不滚动? 最常见原因:父容器没给高度。最外层组件写 flex: 1,列表才能算出可滚动区域。其次是内容本身没超过一屏——那不滚动是正常的。

  • getItemLayout 是干嘛的?不写会怎样? 行高固定时告诉 FlatList 每行多高,它能直接算出任意行的位置,scrollToIndex 精准不闪。不写也能用,FlatList 会动态测量,但跳转和快速滚动时可能闪烁。行高不固定的就别写。

  • FlatList 和 map 的本质区别? map 渲染全部数据,1000 条建 1000 个原生视图,内存爆炸;FlatList 只建窗口内那十几个,滚动时复用。数据量超过一屏就该用 FlatList。

  • 下拉刷新在 Android 上动画样式不一样? 正常。RefreshControl 用的是系统原生刷新控件,iOS 是弹性小球,Android 是 Material 的圆圈。可以用 tintColor(iOS)和 colors(Android)调主题色。

  • 列表滚动时偶发空白闪烁? 滚得太快,FlatList 渲染跟不上。调大 windowSize、减小 maxToRenderPerBatch 换内存,或者简化行内布局、图片用缩略图。还不行就上第 11 课的 FlashList。

错法对照

错法问题正解
长列表 map + ScrollView全量渲染,卡死崩溃换 FlatList
keyExtractor 用 index删除/插入行后界面错乱、状态错位用数据唯一 id
ScrollView 套 FlatList虚拟化失效 + 手势冲突头部内容放 ListHeaderComponent
onEndReached 里直接发请求同一页重复请求好几次loading ref 守卫 + hasMore 判断
刷新失败后没 setRefreshing(false)转圈永远不消失放 finally 里复位
数据更新后列表不刷新直接 push 改了原数组(引用没变)setList([...list, ...newData]) 新数组
在 renderItem 里 () => handlePress(id) 内联函数满天飞每轮渲染新引用,memo 失效子组件 memo + useCallback(第 11 课细讲)

卡点急救

  1. 列表滚动白屏闪烁:行内容太复杂或图片太大。简化 renderItem、图片换缩略图、调大 windowSize。
  2. 加载更多疯狂触发:检查是否有守卫;onEndReachedThreshold 别设太大(0.3 足够);数据少于一屏时 onEndReached 会立即触发,这是预期行为。
  3. 下拉刷新 Android 没动画:确认用的是 refreshControl={<RefreshControl ... />} 且 refreshing 是受控状态。
  4. scrollToIndex 报错或乱跳:没给 getItemLayout 时 FlatList 不知道目标行的位置。行高固定就补 getItemLayout,不固定就先用 scrollToOffset。
  5. SectionList 分组头在 Android 不吸顶:stickySectionHeadersEnabled 主要对 iOS 生效,Android 支持不完善,需要吸顶效果考虑第三方库。

面试常问

  • FlatList 的虚拟化原理? 维护一个以可视区为中心的渲染「窗口」(windowSize 控制窗口是几屏),只创建窗口内行的原生视图;滚动时窗口移动,离开窗口的行视图被销毁,进入窗口的新建。配合 keyExtractor 做行的身份识别,配合 getItemLayout 跳过动态测量。

  • 列表性能怎么优化? 从轻到重:renderItem 保持简单、行组件 React.memo、keyExtractor 用稳定 id、行高固定给 getItemLayout、调 windowSize/maxToRenderPerBatch、图片用缩略图和缓存、最后换 FlashList(回收复用单元格,万级列表必备,第 11 课细讲)。

  • FlatList、SectionList、FlashList 怎么选? 普通长列表 FlatList;带分组头的 SectionList;数据量上千、行复杂、对帧率敏感的上 FlashList。三者的 data 结构不同,迁移时注意。

  • 如何实现聊天界面那种倒置列表? FlatList 设 inverted 属性即可,新消息自动出现在底部,非常适合 IM 场景。

自测三问

  1. FlatList 相比 ScrollView + map 的核心优势是什么?原理是什么?
  2. 实现加载更多需要哪几个状态/守卫?为什么用 ref 做 loading 守卫而不是 state?
  3. 为什么不能把 FlatList 直接放进 ScrollView?想让头部 banner 和列表一起滚动的正确做法是什么?

小结一句话

Flexbox 定布局、FlatList 管长列表——虚拟化只渲染看得见的行,配上刷新、分页和分组,移动端列表就齐活了。

延伸

  • FlatList 官方文档
  • SectionList 官方文档
  • 下一课:平台差异——让同一份代码在 iOS 和 Android 上各自得体。
加载本节练习…
← 上一课核心组件下一课 →平台差异