布局与列表
这一课你将能做到
- 用 Flexbox 在 RN 里实现水平排列、居中、两端对齐、等分等常见布局
- 用 FlatList 渲染千级长列表且保持流畅,配齐分隔线、空状态、下拉刷新、上拉加载
- 用 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 渲染每个分组的头部。
讲清楚要点
- FlatList 是虚拟化列表:只渲染可视区附近一个「窗口」内的行,滚出去的行被回收,这是流畅的根本原因。窗口大小由
windowSize(单位:屏)控制。
- renderItem 尽量轻:行组件复杂时用
React.memo 包一层;不要在 renderItem 里做重计算,数据在进入列表前处理好。
- keyExtractor 必须稳定唯一:用数据的真实 id,别用 index——用 index 时删除一行,后面所有行的 key 全变,RN 会销毁重建整个列表,还可能错位。
- onEndReached 会重复触发:配合 hasMore + loading 守卫做防重,且用 ref 做守卫(同步生效)。
- 嵌套滚动是大坑:ScrollView 套 FlatList 会让虚拟化失效(FlatList 拿到无限高度,一次性渲染全部)。正确做法:头部内容放
ListHeaderComponent,或在 ScrollView 里用 map(仅当数据量确定很小时)。
- 行高固定就给 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 课细讲) |
卡点急救
- 列表滚动白屏闪烁:行内容太复杂或图片太大。简化 renderItem、图片换缩略图、调大 windowSize。
- 加载更多疯狂触发:检查是否有守卫;
onEndReachedThreshold 别设太大(0.3 足够);数据少于一屏时 onEndReached 会立即触发,这是预期行为。
- 下拉刷新 Android 没动画:确认用的是
refreshControl={<RefreshControl ... />} 且 refreshing 是受控状态。
- scrollToIndex 报错或乱跳:没给 getItemLayout 时 FlatList 不知道目标行的位置。行高固定就补 getItemLayout,不固定就先用 scrollToOffset。
- 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 场景。
自测三问
- FlatList 相比 ScrollView + map 的核心优势是什么?原理是什么?
- 实现加载更多需要哪几个状态/守卫?为什么用 ref 做 loading 守卫而不是 state?
- 为什么不能把 FlatList 直接放进 ScrollView?想让头部 banner 和列表一起滚动的正确做法是什么?
小结一句话
Flexbox 定布局、FlatList 管长列表——虚拟化只渲染看得见的行,配上刷新、分页和分组,移动端列表就齐活了。
延伸