开篇:从"一个函数"到"一组函数"

第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(数据)                                            // 列表由数据决定

数据是状态,列表是输出。


六、做一个可滚动、可点击、可扩展的小列表应用

现在我们把前面所有知识组合起来,做一个待办事项应用。功能:

  1. 显示待办列表;
  2. 点击切换完成状态;
  3. 添加新事项;
  4. 删除事项;
  5. 使用 FlatList 渲染;
  6. 拆成多个组件。

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:综合应用题——可扩展列表

题目:创建一个简单的联系人列表应用,要求:

  1. 初始有三个联系人;
  2. 使用 FlatList 渲染;
  3. 点击联系人显示"你点击了某某";
  4. 顶部有一个按钮,点击后添加一个新联系人。

参考答案:

// 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 高效渲染列表。

核心要点:

  1. 组件就是函数,拆组件就是拆函数;
  2. 父传子用 props,子传父用回调函数;
  3. 共享状态放到共同父组件,叫状态提升;
  4. props 可以设默认值、使用 children、展开传递;
  5. 列表用 map 或 FlatList 渲染,key 是项的身份标识;
  6. FlatList 按需渲染,适合长列表;
  7. 列表项应拆成独立组件,通过 props 接收数据和回调;
  8. 不可变更新让状态真正"变新",触发界面重新计算。

记住一句话:

界面是一棵组件树,列表是数据驱动的一组组件。


第4课预告

下一课,我们进入 【React Native娓娓道来】第4课 副作用与数据获取:useEffect、网络请求与加载状态。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐