【React Native娓娓道来】第3课 组件组合与列表渲染:把界面拆成可复用的函数
开篇:从"一个函数"到"一组函数"
第2课我们讲透了一句话:
// 概念表达
视图 = 函数(状态) // 视图是状态经过函数计算出来的结果
一个组件就是一个函数。它接收状态和 props,返回界面描述。状态变,函数重新执行,界面自动更新。
但真实应用不可能只有一个函数。一个页面往往包含头像、标题、列表、按钮、输入框……如果全部塞进一个组件里,这个函数会变得又长又乱,读起来费劲,改起来容易出错。
所以,这一课要解决的问题是:
如何把一个大界面,拆成多个小函数,再把这些小函数组合起来。
同时,我们还会学习 React Native 中处理列表的标准方式:FlatList。
这一课的核心可以概括成两句话:
// 概念表达
界面 = 组件树 // 界面是一棵组件组成的树
列表 = 用数据驱动一组组件 // 列表是数据驱动的一组组件
一、为什么要拆组件
1.1 一个大组件的痛苦
假设要做一个个人主页头部,显示头像、姓名、简介。如果写成一个组件:
// JavaScript + React Native
import { View, Text, Image, StyleSheet } from 'react-native'; // 引入 View、Text、Image、StyleSheet
export default function ProfileHeader() { // 定义并默认导出 ProfileHeader 组件
return ( // 返回界面描述
<View style={styles.header}> // 头部容器,应用 header 样式
<Image // 头像图片
source={{ uri: 'https://example.com/avatar.jpg' }} // 网络头像地址
style={styles.avatar} // 应用 avatar 样式
/> // 结束 Image
<View style={styles.info}> // 右侧信息容器
<Text style={styles.name}>小明</Text> // 显示姓名
<Text style={styles.bio}>热爱编程,正在学习 React Native</Text> // 显示简介
</View> // 结束信息容器
</View> // 结束头部容器
); // 结束 return
} // 结束 ProfileHeader 组件
const styles = StyleSheet.create({ // 创建样式表
header: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: '#fff' }, // 头部布局
avatar: { width: 60, height: 60, borderRadius: 30, marginRight: 16 }, // 头像尺寸和圆角
info: { flex: 1 }, // 信息区占满剩余宽度
name: { fontSize: 20, fontWeight: 'bold', marginBottom: 4 }, // 姓名样式
bio: { fontSize: 14, color: '#888' }, // 简介样式
}); // 结束样式表
这个组件还不算长,但已经能看出问题:头像、信息、样式全部混在一起。如果以后要加"关注按钮"“在线状态”“标签列表”,这个组件会迅速膨胀。
1.2 拆成小组件
把头像拆成 Avatar,把信息拆成 UserInfo,再组合成 ProfileHeader:
// JavaScript + React Native
import { View, Text, Image, StyleSheet } from 'react-native'; // 引入需要的组件
function Avatar({ uri }) { // 定义 Avatar 组件,接收 uri
return <Image source={{ uri }} style={styles.avatar} />; // 返回一个图片,应用 avatar 样式
} // 结束 Avatar 组件
function UserInfo({ name, bio }) { // 定义 UserInfo 组件,接收 name 和 bio
return ( // 返回界面描述
<View style={styles.info}> // 信息容器
<Text style={styles.name}>{name}</Text> // 显示姓名
<Text style={styles.bio}>{bio}</Text> // 显示简介
</View> // 结束信息容器
); // 结束 return
} // 结束 UserInfo 组件
export default function ProfileHeader() { // 定义并默认导出 ProfileHeader 组件
return ( // 返回界面描述
<View style={styles.header}> // 头部容器
<Avatar uri="https://example.com/avatar.jpg" /> // 使用 Avatar,传入头像地址
<UserInfo name="小明" bio="热爱编程,正在学习 React Native" /> // 使用 UserInfo,传入姓名和简介
</View> // 结束头部容器
); // 结束 return
} // 结束 ProfileHeader 组件
const styles = StyleSheet.create({ // 创建样式表
header: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: '#fff' }, // 头部布局
avatar: { width: 60, height: 60, borderRadius: 30, marginRight: 16 }, // 头像尺寸和圆角
info: { flex: 1 }, // 信息区占满剩余宽度
name: { fontSize: 20, fontWeight: 'bold', marginBottom: 4 }, // 姓名样式
bio: { fontSize: 14, color: '#888' }, // 简介样式
}); // 结束样式表
现在每个组件只负责一小块。Avatar 只管头像,UserInfo 只管姓名和简介,ProfileHeader 负责组合。以后要改头像样式,只改 Avatar;要改信息布局,只改 UserInfo。
1.3 组件就是函数,组合就是函数调用
从"视图 = 函数(状态)"的角度看,拆组件就是拆函数:
Avatar(uri)返回头像视图;UserInfo(name, bio)返回信息视图;ProfileHeader()返回组合后的视图。
组件树本质上是一棵函数调用树。父组件调用子组件,子组件返回更小的界面描述,最终拼成完整界面。
1.4 拆组件的好处
- 可读性:每个组件短小,容易看懂;
- 可复用:
Avatar可以用在个人主页、评论、消息列表; - 可维护:改一处不影响其他部分;
- 可测试:小组件更容易单独验证;
- 可协作:不同人负责不同组件,减少冲突。
拆组件不是目的,而是让代码更接近"视图 = 函数(状态)"的自然表达。
二、父子组件通信
拆完组件后,立刻会遇到一个问题:组件之间怎么传数据?
React Native 的答案很明确:props 向下传,事件向上传。
2.1 父传子:props
父组件通过 props 把数据传给子组件:
// JavaScript + React Native
import { Text } from 'react-native'; // 引入 Text 组件
function Greeting({ name }) { // 定义子组件 Greeting,解构出 name
return <Text>你好,{name}!</Text>; // 返回问候文本,内容由 name 决定
} // 结束 Greeting 组件
export default function App() { // 定义并默认导出 App 组件
return <Greeting name="小明" />; // 使用 Greeting,传入 name="小明"
} // 结束 App 组件
App 是父组件,Greeting 是子组件。name="小明" 就是父传子的数据。子组件只读 props,不直接修改。
2.2 子传父:回调函数
子组件想通知父组件"发生了什么",可以通过父组件传下来的函数:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, Text, Button } from 'react-native'; // 引入 View、Text、Button 组件
function Child({ onAdd }) { // 定义子组件 Child,接收 onAdd 回调
return <Button title="加一" onPress={onAdd} />; // 返回按钮,点击时调用 onAdd
} // 结束 Child 组件
export default function App() { // 定义并默认导出 App 组件
const [count, setCount] = useState(0); // 创建状态 count,初始值 0
return ( // 返回界面描述
<View> // 外层容器
<Text>计数:{count}</Text> // 显示当前计数
<Child onAdd={() => setCount(count + 1)} /> // 使用 Child,传入更新函数
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
父组件把 onAdd 函数传给子组件。子组件点击按钮时调用它。数据(新的 count)由父组件更新,再通过 props 流回子组件。
这就是单向数据流:
// 概念表达
数据向下传:props // 父组件通过 props 把数据传给子组件
事件向上传:回调函数 // 子组件通过回调通知父组件更新
2.3 状态提升
当两个组件需要共享同一个状态时,把这个状态放到它们的共同父组件中,这叫"状态提升"。
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, Text, Button } from 'react-native'; // 引入 View、Text、Button 组件
function Display({ count }) { // 定义子组件 Display,接收 count
return <Text>当前:{count}</Text>; // 显示当前计数
} // 结束 Display 组件
function Controls({ onAdd, onReset }) { // 定义子组件 Controls,接收两个回调
return ( // 返回界面描述
<View> // 外层容器
<Button title="加一" onPress={onAdd} /> // 加一按钮
<Button title="重置" onPress={onReset} /> // 重置按钮
</View> // 结束 View
); // 结束 return
} // 结束 Controls 组件
export default function App() { // 定义并默认导出 App 组件
const [count, setCount] = useState(0); // 创建状态 count,初始值 0
return ( // 返回界面描述
<View> // 外层容器
<Display count={count} /> // 显示区,传入 count
<Controls // 控制区,传入两个回调
onAdd={() => setCount(count + 1)} // 加一回调
onReset={() => setCount(0)} // 重置回调
/> // 结束 Controls
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
count 放在 App 中,Display 只负责显示,Controls 只负责触发更新。两个子组件通过父组件共享状态。
状态提升后,数据流依然是单向的:父组件持有状态,子组件通过 props 读取,通过回调请求更新。
三、Props 进阶
3.1 默认值
函数组件可以用默认参数给 props 设默认值:
// JavaScript + React Native
import { Image } from 'react-native'; // 引入 Image 组件
function Avatar({ size = 60, uri }) { // 定义 Avatar,size 默认值 60
return ( // 返回界面描述
<Image // 图片组件
source={{ uri }} // 图片来源
style={{ width: size, height: size, borderRadius: size / 2 }} // 宽高和圆角都由 size 计算
/> // 结束 Image
); // 结束 return
} // 结束 Avatar 组件
使用时可以不传 size,默认就是 60。
3.2 children
children 是一个特殊的 prop,表示组件标签中间的内容:
// JavaScript + React Native
import { View, Text, StyleSheet } from 'react-native'; // 引入 View、Text、StyleSheet
function Card({ children }) { // 定义 Card 组件,接收 children
return <View style={styles.card}>{children}</View>; // 返回一个容器,把 children 渲染在里面
} // 结束 Card 组件
export default function App() { // 定义并默认导出 App 组件
return ( // 返回界面描述
<Card> // 使用 Card
<Text>标题</Text> // Card 里的第一个子元素
<Text>内容</Text> // Card 里的第二个子元素
</Card> // 结束 Card
); // 结束 return
} // 结束 App 组件
const styles = StyleSheet.create({ // 创建样式表
card: { padding: 16, backgroundColor: '#fff', borderRadius: 8, margin: 16 }, // card 样式
}); // 结束样式表
Card 不关心里面是什么,只负责提供外框和样式。这种组件叫"容器组件"或"布局组件"。
3.3 展开传递 props
如果父组件想把一批 props 直接传给子组件,可以用展开运算符:
// JavaScript + React Native
import { View, Text } from 'react-native'; // 引入 View、Text 组件
function UserInfo({ name, bio, age }) { // 定义 UserInfo,接收三个 props
return ( // 返回界面描述
<View> // 外层容器
<Text>{name}</Text> // 显示姓名
<Text>{bio}</Text> // 显示简介
<Text>{age}</Text> // 显示年龄
</View> // 结束 View
); // 结束 return
} // 结束 UserInfo 组件
export default function App() { // 定义并默认导出 App 组件
const user = { name: '小明', bio: '学习者', age: 18 }; // 定义用户对象
return <UserInfo {...user} />; // 用展开运算符把 user 的属性作为 props 传入
} // 结束 App 组件
{...user} 等价于 name={user.name} bio={user.bio} age={user.age}。
3.4 传递函数
函数作为 props 传递时,注意不要直接调用:
// JavaScript + React Native (JSX)
// 错误:渲染时立即调用
<Button title="点我" onPress={handlePress()} /> // handlePress() 会立即执行,把返回值传给 onPress
// 正确:把函数本身传进去
<Button title="点我" onPress={handlePress} /> // 传的是函数本身,点击时才执行
如果需要传参数,可以用箭头函数包一层:
// JavaScript + React Native (JSX)
<Button title="加一" onPress={() => setCount(count + 1)} /> // 用箭头函数包一层,点击时才执行 setCount
3.5 props 只读
子组件不应该修改 props:
// JavaScript + React Native
// 错误
function Child({ count }) { // 定义子组件
count = count + 1; // 直接修改 props,不允许
return <Text>{count}</Text>; // 返回文本
} // 结束组件
props 是父组件的数据,子组件只能读取。要改,应该通过父组件传下来的回调函数去改。
四、列表渲染基础
4.1 用 map 渲染数组
React Native 中渲染列表最基础的方式是 map:
// JavaScript + React Native
import { View, Text } from 'react-native'; // 引入 View、Text 组件
export default function App() { // 定义并默认导出 App 组件
const fruits = ['苹果', '香蕉', '橙子']; // 定义数组
return ( // 返回界面描述
<View> // 外层容器
{fruits.map((fruit) => ( // 遍历数组,每个元素生成一个 Text
<Text key={fruit}>{fruit}</Text> // key 用水果名,显示水果名
))} // 结束 map
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
map 把数组中的每一项转换成一个 <Text>,最终得到一组视图描述。
4.2 key 的作用
每个列表项都需要一个 key。key 是 React 用来识别列表项的身份标识。
// JavaScript + React Native (JSX)
<Text key={fruit}>{fruit}</Text> // key 用水果名,帮助 React 识别每一项
当列表发生变化时,React 通过 key 判断哪些项是新增的、删除的、移动的,从而高效更新界面。
key 最好用稳定唯一的 id:
// JavaScript + React Native (JSX)
{todos.map((todo) => ( // 遍历 todos
<Text key={todo.id}>{todo.text}</Text> // key 用 todo.id
))} // 结束 map
如果列表会动态增删,尽量不要用数组索引当 key:
// 不推荐:列表增删时可能出问题
{todos.map((todo, index) => ( // 遍历 todos,带 index
<Text key={index}>{todo.text}</Text> // key 用 index,列表增删时身份会错乱
))} // 结束 map
因为索引会变,React 可能误判项的身份。
4.3 列表项组件化
列表项通常要拆成单独组件:
// JavaScript + React Native
import { Text, StyleSheet } from 'react-native'; // 引入 Text、StyleSheet
function TodoItem({ text, done, onPress }) { // 定义 TodoItem,接收 text、done、onPress
return ( // 返回界面描述
<Text // 文本组件
style={[styles.item, done && styles.done]} // 组合样式,完成时叠加删除线
onPress={onPress} // 点击时调用 onPress
> // 开始 Text 标签
{done ? '✓ ' : '○ '}{text} // 前缀符号 + 文本
</Text> // 结束 Text 标签
); // 结束 return
} // 结束 TodoItem 组件
const styles = StyleSheet.create({ // 创建样式表
item: { fontSize: 18, padding: 12 }, // 待办项样式
done: { textDecorationLine: 'line-through', color: '#aaa' }, // 完成样式:删除线 + 灰色
}); // 结束样式表
然后父组件用 map 渲染:
// JavaScript + React Native (JSX)
{todos.map((todo) => ( // 遍历 todos
<TodoItem // 每条渲染一个 TodoItem
key={todo.id} // key 用唯一 id
text={todo.text} // 传入文本
done={todo.done} // 传入完成状态
onPress={() => toggleTodo(todo.id)} // 传入点击回调
/> // 结束 TodoItem
))} // 结束 map
这样列表项的逻辑和样式都封装在 TodoItem 中。
五、FlatList 高效渲染列表
5.1 为什么需要 FlatList
用 ScrollView + map 可以渲染列表,但有一个问题:ScrollView 会一次性渲染所有子项。如果列表有几百上千条,性能会很差。
FlatList 是 React Native 提供的高效列表组件。它只渲染屏幕上可见的项,滚出屏幕的项会被回收。这让长列表也能流畅滚动。
5.2 基本用法
// JavaScript + React Native
import { FlatList, Text } from 'react-native'; // 引入 FlatList、Text 组件
export default function App() { // 定义并默认导出 App 组件
const fruits = [ // 定义数据数组
{ id: '1', name: '苹果' }, // 第一条数据
{ id: '2', name: '香蕉' }, // 第二条数据
{ id: '3', name: '橙子' }, // 第三条数据
]; // 结束数组
return ( // 返回界面描述
<FlatList // 高效列表组件
data={fruits} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={({ item }) => <Text>{item.name}</Text>} // 如何渲染每一项
/> // 结束 FlatList
); // 结束 return
} // 结束 App 组件
FlatList 的三个核心 props:
data:数据数组;keyExtractor:从每一项中提取唯一 key;renderItem:如何渲染每一项。
renderItem 接收一个参数,通常写成 { item },其中 item 是当前项的数据。
5.3 列表项点击
用 Pressable 包裹列表项,处理点击:
// JavaScript + React Native
import { FlatList, Text, Pressable, StyleSheet } from 'react-native'; // 引入需要的组件
function TodoItem({ text, done, onPress }) { // 定义 TodoItem,接收 text、done、onPress
return ( // 返回界面描述
<Pressable onPress={onPress}> // 可点击容器,点击时调用 onPress
<Text style={done ? styles.done : null}> // 文本,完成时应用删除线样式
{done ? '✓ ' : '○ '}{text} // 前缀符号 + 文本
</Text> // 结束 Text
</Pressable> // 结束 Pressable
); // 结束 return
} // 结束 TodoItem 组件
export default function App() { // 定义并默认导出 App 组件
const todos = [ // 定义待办数组
{ id: '1', text: '学习 FlatList', done: false }, // 第一条,未完成
{ id: '2', text: '完成练习', done: true }, // 第二条,已完成
]; // 结束数组
return ( // 返回界面描述
<FlatList // 高效列表组件
data={todos} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<TodoItem // 渲染 TodoItem
text={item.text} // 传入文本
done={item.done} // 传入完成状态
onPress={() => {}} // 传入点击回调
/> // 结束 TodoItem
)} // 结束 renderItem
/> // 结束 FlatList
); // 结束 return
} // 结束 App 组件
const styles = StyleSheet.create({ // 创建样式表
done: { textDecorationLine: 'line-through', color: '#aaa' }, // 完成样式:删除线 + 灰色
}); // 结束样式表
5.4 常用附加功能
FlatList 还支持很多实用功能:
// JavaScript + React Native (JSX)
ItemSeparatorComponent={() => <View style={styles.separator} />} // 每项之间的分隔线
ListEmptyComponent={<Text>暂无数据</Text>} // 数据为空时显示的内容
ListHeaderComponent={<Text>头部</Text>} // 列表顶部的组件
ListFooterComponent={<Text>尾部</Text>} // 列表底部的组件
refreshing={refreshing} // 是否正在刷新
onRefresh={handleRefresh} // 下拉时触发的回调
这些功能让 FlatList 能覆盖大多数列表场景。
5.5 FlatList 与"视图 = 函数(状态)"
FlatList 本身也是声明式的。你给它 data,它根据数据渲染列表。数据变,列表变。你不需要手动创建或删除列表项。
// 概念表达
列表视图 = FlatList(数据) // 列表由数据决定
数据是状态,列表是输出。
六、做一个可滚动、可点击、可扩展的小列表应用
现在我们把前面所有知识组合起来,做一个待办事项应用。功能:
- 显示待办列表;
- 点击切换完成状态;
- 添加新事项;
- 删除事项;
- 使用 FlatList 渲染;
- 拆成多个组件。
6.1 拆组件
TodoItem:单条待办;AddTodo:输入框和添加按钮;TodoList:列表;App:持有状态,组合一切。
6.2 完整代码
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { // 引入需要的组件
View, // 容器
Text, // 文本
TextInput, // 输入框
Button, // 按钮
FlatList, // 高效列表
Pressable, // 可点击容器
StyleSheet, // 样式表
} from 'react-native'; // 从 react-native 导入
function TodoItem({ text, done, onToggle, onDelete }) { // 定义 TodoItem,接收文本、完成状态和两个回调
return ( // 返回界面描述
<View style={styles.itemRow}> // 每行的容器
<Pressable style={styles.itemMain} onPress={onToggle}> // 左侧主区域,点击切换完成状态
<Text style={[styles.itemText, done && styles.itemDone]}> // 文本,完成时叠加删除线样式
{done ? '✓ ' : '○ '}{text} // 前缀符号 + 文本
</Text> // 结束 Text
</Pressable> // 结束 Pressable
<Pressable onPress={onDelete}> // 右侧删除按钮
<Text style={styles.delete}>删除</Text> // 删除文字
</Pressable> // 结束 Pressable
</View> // 结束行容器
); // 结束 return
} // 结束 TodoItem 组件
function AddTodo({ onAdd }) { // 定义 AddTodo,接收 onAdd 回调
const [text, setText] = useState(''); // 创建状态 text,初始空字符串
const handleAdd = () => { // 定义添加函数
if (text.trim() === '') return; // 空字符串不添加
onAdd(text.trim()); // 调用父组件传入的 onAdd
setText(''); // 清空输入框
}; // 结束 handleAdd
return ( // 返回界面描述
<View style={styles.addRow}> // 添加行容器
<TextInput // 输入框
style={styles.input} // 应用 input 样式
value={text} // 值由 text 决定
onChangeText={setText} // 输入变化时更新 text
placeholder="输入新事项" // 占位提示
/> // 结束 TextInput
<Button title="添加" onPress={handleAdd} /> // 添加按钮
</View> // 结束添加行
); // 结束 return
} // 结束 AddTodo 组件
export default function App() { // 定义并默认导出 App 组件
const [todos, setTodos] = useState([ // 创建状态 todos,初始三条数据
{ id: '1', text: '学习组件组合', done: false }, // 第一条,未完成
{ id: '2', text: '学习 FlatList', done: false }, // 第二条,未完成
{ id: '3', text: '完成练习', done: true }, // 第三条,已完成
]); // 结束 useState
const toggleTodo = (id) => { // 定义切换完成状态的函数
setTodos( // 更新 todos
todos.map((todo) => // 遍历每一项
todo.id === id ? { ...todo, done: !todo.done } : todo // id 匹配则翻转 done,否则原样返回
) // 结束 map
); // 结束 setTodos
}; // 结束 toggleTodo
const deleteTodo = (id) => { // 定义删除函数
setTodos(todos.filter((todo) => todo.id !== id)); // 用 filter 过滤掉指定 id
}; // 结束 deleteTodo
const addTodo = (text) => { // 定义添加函数
const newId = Date.now().toString(); // 用时间戳作为新 id
setTodos([...todos, { id: newId, text, done: false }]); // 用展开运算符追加新事项
}; // 结束 addTodo
return ( // 返回界面描述
<View style={styles.container}> // 外层容器
<Text style={styles.heading}>我的待办</Text> // 标题
<AddTodo onAdd={addTodo} /> // 添加组件,传入添加回调
<FlatList // 高效列表
data={todos} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<TodoItem // 渲染 TodoItem
text={item.text} // 传入文本
done={item.done} // 传入完成状态
onToggle={() => toggleTodo(item.id)} // 传入切换回调
onDelete={() => deleteTodo(item.id)} // 传入删除回调
/> // 结束 TodoItem
)} // 结束 renderItem
ListEmptyComponent={<Text style={styles.empty}>暂无待办</Text>} // 空列表提示
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/> // 结束 FlatList
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, padding: 20, paddingTop: 60 }, // 容器样式
heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 }, // 标题样式
addRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 }, // 添加行样式
input: { // 输入框样式
flex: 1, // 占满剩余宽度
borderWidth: 1, // 边框宽度 1
borderColor: '#ccc', // 边框颜色
borderRadius: 6, // 圆角 6
paddingHorizontal: 10, // 水平内边距 10
height: 40, // 高度 40
marginRight: 8, // 右侧间距 8
}, // 结束 input 样式
itemRow: { // 列表行样式
flexDirection: 'row', // 水平排列
alignItems: 'center', // 垂直居中
justifyContent: 'space-between', // 两端对齐
paddingVertical: 12, // 垂直内边距 12
}, // 结束 itemRow 样式
itemMain: { flex: 1 }, // 主区域占满剩余宽度
itemText: { fontSize: 18 }, // 文本字号 18
itemDone: { textDecorationLine: 'line-through', color: '#aaa' }, // 完成样式:删除线 + 灰色
delete: { color: '#d00', fontSize: 14 }, // 删除按钮样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
empty: { textAlign: 'center', color: '#999', marginTop: 40 }, // 空列表提示样式
}); // 结束样式表
6.3 代码解读
App持有todos状态,是唯一数据源;TodoItem只负责显示一条待办,通过 props 接收数据和回调;AddTodo自己管理输入框状态text,添加时调用onAdd把文本传给父组件;toggleTodo用map生成新数组,用展开运算符生成新对象,实现不可变更新;deleteTodo用filter生成新数组;addTodo用展开运算符追加新事项;FlatList用data、keyExtractor、renderItem渲染列表;ListEmptyComponent处理空列表;ItemSeparatorComponent添加分隔线。
这个应用虽小,但已经具备组件组合、状态提升、单向数据流、不可变更新、列表渲染等核心模式。
七、练习题
练习题1:选择题——拆组件的好处
题目:下面哪一项不是拆组件的主要好处?
A. 提高可读性
B. 方便复用
C. 让状态自动更新
D. 便于维护
参考答案:C
解读:拆组件能提高可读性、复用性和可维护性,但状态自动更新是 React Native 的声明式机制带来的,不是拆组件本身的功能。
练习题2:填空题——父子通信
题目:父组件向子组件传数据用 ______,子组件向父组件通知事件用 ______。
参考答案:props;回调函数。
解读:React Native 遵循单向数据流:数据通过 props 向下传,事件通过父组件传下来的回调函数向上传。
练习题3:改错题——直接修改 props
题目:下面代码有一处错误,请指出并修正。
// JavaScript + React Native
import { Text } from 'react-native'; // 引入 Text 组件
function Child({ count }) { // 定义子组件 Child,接收 count
count = count + 1; // 错误:直接修改 props
return <Text>{count}</Text>; // 返回文本
} // 结束 Child 组件
参考答案:子组件不应修改 props。应该通过父组件传下来的回调函数更新状态。
解读:props 是只读的。子组件只能读取,不能直接赋值修改。
练习题4:代码阅读题——children
题目:阅读代码,说出屏幕上显示什么。
// JavaScript + React Native
import { View, Text } from 'react-native'; // 引入 View、Text 组件
function Card({ children }) { // 定义 Card 组件,接收 children
return <View style={{ padding: 16 }}>{children}</View>; // 返回容器,把 children 渲染在里面
} // 结束 Card 组件
export default function App() { // 定义并默认导出 App 组件
return ( // 返回界面描述
<Card> // 使用 Card
<Text>标题</Text> // 第一个子元素
<Text>内容</Text> // 第二个子元素
</Card> // 结束 Card
); // 结束 return
} // 结束 App 组件
参考答案:屏幕上显示"标题"和"内容"两行文字。
解读:children 是组件标签中间的内容。Card 负责提供容器,具体内容由使用者决定。
练习题5:动手题——默认 props
题目:创建一个 Avatar 组件,接收 size 和 uri,size 默认值为 50。
参考答案:
// JavaScript + React Native
import { Image } from 'react-native'; // 引入 Image 组件
function Avatar({ size = 50, uri }) { // 定义 Avatar,size 默认 50
return ( // 返回界面描述
<Image // 图片组件
source={{ uri }} // 图片来源
style={{ width: size, height: size, borderRadius: size / 2 }} // 宽高和圆角由 size 计算
/> // 结束 Image
); // 结束 return
} // 结束 Avatar 组件
解读:函数组件可以用默认参数给 props 设默认值。调用时不传 size,就会使用 50。
练习题6:简答题——状态提升
题目:什么是状态提升?什么时候需要它?
参考答案:状态提升是把多个组件需要共享的状态放到它们的共同父组件中。当两个或多个子组件需要读写同一个状态时,就需要状态提升。
解读:状态提升后,父组件持有状态,通过 props 向下传数据,通过回调向上接收事件。这样数据流仍然是单向的。
练习题7:列表渲染题——map 与 key
题目:有一个数组 const fruits = ['苹果', '香蕉', '橙子'],请用 map 渲染成一组 <Text>,并加上合适的 key。
参考答案:
// JavaScript + React Native (JSX)
{fruits.map((fruit) => ( // 遍历 fruits
<Text key={fruit}>{fruit}</Text> // key 用水果名,显示水果名
))} // 结束 map
解读:map 把数组转换成一组视图描述。key 帮助 React 识别每一项。如果数据有唯一 id,优先用 id。
练习题8:key 的作用题
题目:为什么列表会动态增删时,不建议用数组索引作为 key?
参考答案:因为数组索引会随着增删变化。React 可能把原来的项误认为新的项,导致状态错乱或渲染错误。最好使用稳定唯一的 id。
解读:key 是 React 判断项身份的依据。索引不稳定,身份就不稳定。
练习题9:FlatList 基础题
题目:补全 FlatList 代码,使它能渲染 data 中的每一项。
// JavaScript + React Native (JSX)
<FlatList // 高效列表组件
data={data} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={({ item }) => ____} // 补全渲染逻辑
/> // 结束 FlatList
参考答案:<Text>{item.name}</Text>(根据实际数据结构填写)。
解读:renderItem 决定每一项怎么渲染。它接收 { item },返回对应的 JSX。
练习题10:FlatList 与 ScrollView 区别题
题目:FlatList 和 ScrollView 渲染列表时,主要区别是什么?
参考答案:ScrollView 会一次性渲染所有子项;FlatList 只渲染屏幕上可见的项,适合长列表,性能更好。
解读:列表很长时,应优先使用 FlatList。ScrollView 更适合内容不多的页面。
练习题11:列表项点击题
题目:用 Pressable 包裹文本,点击时调用 onPress。
参考答案:
// JavaScript + React Native (JSX)
<Pressable onPress={onPress}> // 可点击容器,点击时调用 onPress
<Text>点击我</Text> // 内部文本
</Pressable> // 结束 Pressable
解读:Pressable 是处理点击的常用组件。它比 Button 更灵活,可以包裹任意内容。
练习题12:状态提升实战题
题目:父组件有 count 状态,子组件 Display 显示它,子组件 Controls 提供加一按钮。请写出关键代码。
参考答案:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, Text, Button } from 'react-native'; // 引入 View、Text、Button 组件
function Display({ count }) { // 定义 Display,接收 count
return <Text>计数:{count}</Text>; // 显示当前计数
} // 结束 Display 组件
function Controls({ onAdd }) { // 定义 Controls,接收 onAdd
return <Button title="加一" onPress={onAdd} />; // 加一按钮,点击调用 onAdd
} // 结束 Controls 组件
export default function App() { // 定义并默认导出 App 组件
const [count, setCount] = useState(0); // 创建状态 count,初始值 0
return ( // 返回界面描述
<View> // 外层容器
<Display count={count} /> // 显示区,传入 count
<Controls onAdd={() => setCount(count + 1)} /> // 控制区,传入更新回调
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
解读:状态放在父组件,两个子组件通过 props 和回调与父组件通信。
练习题13:不可变更新题——删除项
题目:有一个 todos 数组,请用 filter 删除 id 为 targetId 的项。
参考答案:
// JavaScript + React Native
setTodos(todos.filter((todo) => todo.id !== targetId)); // 用 filter 过滤掉指定 id
解读:filter 返回一个新数组,不修改原数组。这符合不可变更新原则。
练习题14:综合组件拆分题
题目:把下面这个大组件拆成 Header、Body、Footer 三个子组件。
// JavaScript + React Native
export default function App() { // 定义并默认导出 App 组件
return ( // 返回界面描述
<View> // 外层容器
<Text>标题</Text> // 标题文字
<Text>内容</Text> // 内容文字
<Text>底部</Text> // 底部文字
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
参考答案:
// JavaScript + React Native
import { View, Text } from 'react-native'; // 引入 View、Text 组件
function Header() { // 定义 Header 组件
return <Text>标题</Text>; // 返回标题文本
} // 结束 Header 组件
function Body() { // 定义 Body 组件
return <Text>内容</Text>; // 返回内容文本
} // 结束 Body 组件
function Footer() { // 定义 Footer 组件
return <Text>底部</Text>; // 返回底部文本
} // 结束 Footer 组件
export default function App() { // 定义并默认导出 App 组件
return ( // 返回界面描述
<View> // 外层容器
<Header /> // 使用 Header
<Body /> // 使用 Body
<Footer /> // 使用 Footer
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
解读:每个子组件负责一小块界面,父组件负责组合。这样更清晰,也更容易复用和修改。
练习题15:综合应用题——可扩展列表
题目:创建一个简单的联系人列表应用,要求:
- 初始有三个联系人;
- 使用
FlatList渲染; - 点击联系人显示"你点击了某某";
- 顶部有一个按钮,点击后添加一个新联系人。
参考答案:
// JavaScript + React Native
import { useState } from 'react'; // 引入 useState
import { View, Text, Button, FlatList, Pressable, StyleSheet } from 'react-native'; // 引入需要的组件
function ContactItem({ name, onPress }) { // 定义 ContactItem,接收 name 和 onPress
return ( // 返回界面描述
<Pressable onPress={onPress} style={styles.item}> // 可点击容器,应用 item 样式
<Text style={styles.name}>{name}</Text> // 显示联系人姓名
</Pressable> // 结束 Pressable
); // 结束 return
} // 结束 ContactItem 组件
export default function App() { // 定义并默认导出 App 组件
const [contacts, setContacts] = useState([ // 创建状态 contacts,初始三条数据
{ id: '1', name: '小明' }, // 第一个联系人
{ id: '2', name: '小红' }, // 第二个联系人
{ id: '3', name: '小刚' }, // 第三个联系人
]); // 结束 useState
const addContact = () => { // 定义添加联系人函数
const newId = Date.now().toString(); // 用时间戳作为新 id
setContacts([ // 更新 contacts
...contacts, // 展开原有数组
{ id: newId, name: `联系人 ${newId.slice(-4)}` }, // 追加新联系人
]); // 结束 setContacts
}; // 结束 addContact
return ( // 返回界面描述
<View style={styles.container}> // 外层容器
<Button title="添加联系人" onPress={addContact} /> // 添加按钮
<FlatList // 高效列表
data={contacts} // 数据源
keyExtractor={(item) => item.id} // 从每项提取唯一 key
renderItem={({ item }) => ( // 如何渲染每一项
<ContactItem // 渲染 ContactItem
name={item.name} // 传入姓名
onPress={() => alert(`你点击了${item.name}`)} // 传入点击回调
/> // 结束 ContactItem
)} // 结束 renderItem
ItemSeparatorComponent={() => <View style={styles.separator} />} // 分隔线
/> // 结束 FlatList
</View> // 结束 View
); // 结束 return
} // 结束 App 组件
const styles = StyleSheet.create({ // 创建样式表
container: { flex: 1, padding: 20, paddingTop: 60 }, // 容器样式
item: { paddingVertical: 14 }, // 列表项样式
name: { fontSize: 18 }, // 姓名样式
separator: { height: 1, backgroundColor: '#eee' }, // 分隔线样式
}); // 结束样式表
解读:contacts 是状态,FlatList 根据它渲染列表。ContactItem 负责单条显示和点击。addContact 用展开运算符追加新联系人。点击联系人时弹出提示。整个应用体现了组件组合、状态提升、不可变更新和列表渲染。
八、总结
这一课我们学习了如何把界面拆成可复用的函数,并用 FlatList 高效渲染列表。
核心要点:
- 组件就是函数,拆组件就是拆函数;
- 父传子用 props,子传父用回调函数;
- 共享状态放到共同父组件,叫状态提升;
- props 可以设默认值、使用 children、展开传递;
- 列表用
map或FlatList渲染,key是项的身份标识; FlatList按需渲染,适合长列表;- 列表项应拆成独立组件,通过 props 接收数据和回调;
- 不可变更新让状态真正"变新",触发界面重新计算。
记住一句话:
界面是一棵组件树,列表是数据驱动的一组组件。
第4课预告
下一课,我们进入 【React Native娓娓道来】第4课 副作用与数据获取:useEffect、网络请求与加载状态。
更多推荐



所有评论(0)