开篇:渲染之外的那些事

前三课我们反复强调一句话:

// 概念表达
视图 = 函数(状态)                                                    // 视图是状态经过函数计算出来的结果

组件是函数,接收状态,返回视图描述。状态变,函数重新执行,界面自动更新。

但真实应用不只是"根据状态显示界面"。打开页面要从服务器拉数据,提交表单要发请求,倒计时需要定时器,页面离开要取消未完成的请求。这些事情有一个共同点:它们不是"计算界面",而是"在界面之外做点什么"。

在 React Native 中,这类事情叫副作用。

这一课要解决的问题是:

副作用放在哪里?什么时候执行?做完之后,状态怎么更新,界面怎么跟着变?


一、渲染函数必须是纯的

1.1 什么是纯函数

纯函数满足两个条件:相同输入得到相同输出;不修改外部变量,不做副作用。

// JavaScript
function add(a, b) {                                                // 定义纯函数 add
  return a + b;                                                     // 返回两数之和
}                                                                   // 结束 add

输入 1 和 2,永远返回 3。它不修改外部变量,也不发请求、不写文件。

1.2 为什么渲染必须纯

组件函数会被执行很多次。每次状态变化、props 变化,组件都会重新执行。如果渲染过程中做了副作用,会出问题。

// JavaScript + React Native
// 错误:渲染中发请求
function UserProfile() {                                            // 定义组件
  fetch('https://api.example.com/user');                            // 渲染中发请求
  return <Text>用户信息</Text>;                                     // 返回界面描述
}                                                                   // 结束组件

每次重新渲染都会发一次请求。状态一变、父组件一更新、热重载一次,请求就多一次。结果就是:请求满天飞,数据混乱,性能下降。

// JavaScript + React Native
// 错误:渲染中修改外部变量
let total = 0;                                                      // 外部变量

function Counter() {                                                // 定义组件
  total = total + 1;                                                // 渲染中修改外部变量
  return <Text>{total}</Text>;                                      // 返回界面描述
}                                                                   // 结束组件

每次渲染都改 total,界面变得不可预测。

所以,React Native 要求:渲染过程只做一件事——根据当前状态,计算并返回视图描述。

那么网络请求、定时器、订阅这些事放哪里?答案是:useEffect。


二、useEffect:副作用的归宿

2.1 基本用法

// JavaScript + React Native
import { useEffect } from 'react';                                  // 引入 useEffect Hook
import { Text } from 'react-native';                                // 引入 Text 组件

function App() {                                                    // 定义函数组件 App
  useEffect(() => {                                                 // 注册副作用
    console.log('组件渲染完成了');                                   // 副作用内容
  });                                                               // 不传依赖数组,每次渲染后执行

  return <Text>你好</Text>;                                         // 返回界面描述
}                                                                   // 结束 App

useEffect 接收一个函数,这个函数在组件渲染完成之后执行。注意顺序:

// 概念表达
渲染 -> 返回视图描述 -> 界面更新 -> 执行 useEffect                         // useEffect 在渲染完成后执行

不是渲染之前,也不是渲染之中,而是渲染之后。

2.2 三种依赖数组

第二个参数是依赖数组,决定副作用什么时候执行。

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log('每次渲染后都执行');                                   // 副作用内容
});                                                                 // 不传依赖数组:每次渲染后都执行

useEffect(() => {                                                   // 副作用回调
  console.log('只在挂载后执行一次');                                 // 副作用内容
}, []);                                                             // 空数组:只在挂载后执行一次

useEffect(() => {                                                   // 副作用回调
  console.log('count 变化时执行');                                   // 副作用内容
}, [count]);                                                        // 具体依赖:挂载后 + count 变化时
依赖数组执行时机
不传每次渲染后
[]只在挂载后一次
[a, b]挂载后,以及 a 或 b 变化时

大多数场景用的是后两种。

2.3 依赖数组比较的是什么

React 用 Object.is 比较依赖数组里每一项的上一次值和这一次值。

  • 基本类型(数字、字符串、布尔值):比较值;
  • 对象或数组:比较引用。

所以,依赖里放对象或数组,而每次渲染又都创建了新对象,useEffect 会每次执行:

// JavaScript + React Native
// 反例:每次渲染都创建新对象,导致 useEffect 每次执行
useEffect(() => {                                                   // 副作用回调
  console.log('会频繁执行');                                         // 副作用内容
}, [{ id: 1 }]);                                                    // 每次渲染都创建新对象

正确做法是依赖具体的字段:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log(user.id);                                             // 使用 user.id
}, [user.id]);                                                      // 依赖具体的字段

2.4 一个完整的执行流程

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, Button } from 'react-native';                  // 引入 View、Text、Button

export default function App() {                                     // 定义并默认导出 App
  const [count, setCount] = useState(0);                            // 创建状态 count,初始 0

  useEffect(() => {                                                 // 注册副作用
    console.log(`count 现在是 ${count}`);                            // 打印当前 count
  }, [count]);                                                      // 依赖 count

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>你点击了 {count} 次</Text>                               // 显示点击次数
      <Button title="加一" onPress={() => setCount(count + 1)} />   // 点击加一
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App

流程:

  1. 首次渲染,count 是 0;
  2. 渲染完成后,useEffect 执行,打印"count 现在是 0";
  3. 点击按钮,count 变成 1;
  4. 组件重新渲染;
  5. 因为 count 变了,useEffect 再次执行,打印"count 现在是 1"。

useEffect 里的代码不会阻塞渲染,它是渲染完成之后的"后续动作"。


三、清理函数:副作用也要善后

3.1 为什么需要清理

有些副作用需要善后:定时器要清除,订阅要取消,请求要中止。如果不管,可能导致内存泄漏、重复执行,或者在已卸载的组件上更新状态。

3.2 返回清理函数

useEffect 的回调可以返回一个函数,这个函数就是清理函数。

// JavaScript + React Native
import { useEffect } from 'react';                                  // 引入 useEffect

useEffect(() => {                                                   // 副作用回调
  const timer = setInterval(() => {                                 // 启动定时器
    console.log('每秒执行一次');                                     // 每秒打印
  }, 1000);                                                         // 间隔 1000 毫秒

  return () => {                                                    // 返回清理函数
    clearInterval(timer);                                           // 清除定时器
    console.log('定时器已清除');                                     // 打印清理日志
  };                                                                // 结束清理函数
}, []);                                                             // 空数组,只执行一次

执行顺序:

  1. 组件挂载后,启动定时器;
  2. 组件卸载前,执行清理函数,清除定时器。

如果依赖数组里有值,清理函数的执行时机是:

  1. 依赖变化导致副作用重新执行之前,先执行上一次的清理函数;
  2. 组件卸载前,执行最后一次清理函数。
// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log(`进入 count = ${count}`);                             // 副作用开始

  return () => {                                                    // 返回清理函数
    console.log(`离开 count = ${count}`);                           // 副作用结束
  };                                                                // 结束清理函数
}, [count]);                                                        // 依赖 count

每次 count 变化,都会先"离开"旧值,再"进入"新值。

一句话理解:每次副作用开始之前,先把上一次的副作用收拾干净。


四、用 useEffect 获取数据

4.1 最简单的数据获取

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, ActivityIndicator } from 'react-native';       // 引入 View、Text、ActivityIndicator

export default function App() {                                     // 定义并默认导出 App
  const [data, setData] = useState(null);                           // 创建状态 data,初始 null
  const [loading, setLoading] = useState(true);                     // 创建状态 loading,初始 true

  useEffect(() => {                                                 // 注册副作用
    async function load() {                                         // 定义异步加载函数
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
      const json = await res.json();                                // 解析 JSON
      setData(json);                                                // 更新 data
      setLoading(false);                                            // 关闭 loading
    }                                                               // 结束 load

    load();                                                         // 调用 load
  }, []);                                                           // 空数组,只执行一次

  if (loading) {                                                    // 还在加载
    return <ActivityIndicator />;                                   // 显示加载指示器
  }                                                                 // 结束判断

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>{data.title}</Text>                                     // 显示数据标题
    </View>                                                         // 结束 View
  );                                                                // 结束 return
}                                                                   // 结束 App

流程:首次渲染 loading 为 true,显示加载指示器。渲染完成后 useEffect 执行,发请求。请求返回后更新 data 和 loading,组件重新渲染,显示数据。

4.2 为什么不能直接写 async

// JavaScript + React Native
// 错误示例
useEffect(async () => {                                             // async 回调,不允许
  const res = await fetch('https://api.example.com');               // 发请求
  const json = await res.json();                                    // 解析 JSON
  setData(json);                                                    // 更新数据
}, []);                                                             // 空数组

useEffect 的回调应该返回清理函数或 undefined。写成 async 会返回 Promise,React 无法识别,会给出警告。

正确做法是在回调里定义一个 async 函数,再调用它:

// JavaScript + React Native
useEffect(() => {                                                   // 普通回调
  async function load() {                                           // 内部定义 async 函数
    const res = await fetch('https://api.example.com');             // 发请求
    const json = await res.json();                                  // 解析 JSON
    setData(json);                                                  // 更新数据
  }                                                                 // 结束 load

  load();                                                           // 调用 load
}, []);                                                             // 空数组

4.3 完整的三态:加载中、成功、失败

真实的数据获取通常有三种状态。用一个变量管理会乱,用三个更清楚:

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, ActivityIndicator, Button, StyleSheet } from 'react-native';  // 引入需要的组件

export default function App() {                                     // 定义并默认导出 App
  const [data, setData] = useState(null);                           // 数据状态,初始 null
  const [loading, setLoading] = useState(true);                     // 加载状态,初始 true
  const [error, setError] = useState(null);                         // 错误状态,初始 null

  const load = async () => {                                        // 定义加载函数
    try {                                                           // 开始 try
      setLoading(true);                                             // 打开 loading
      setError(null);                                               // 清空 error
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
      if (!res.ok) throw new Error(`请求失败:${res.status}`);      // 检查状态码
      const json = await res.json();                                // 解析 JSON
      setData(json);                                                // 更新 data
    } catch (e) {                                                   // 捕获错误
      setError(e.message);                                          // 更新 error
    } finally {                                                     // 收尾
      setLoading(false);                                            // 关闭 loading
    }                                                               // 结束 try
  };                                                                // 结束 load

  useEffect(() => {                                                 // 注册副作用
    load();                                                         // 首次加载
  }, []);                                                           // 空数组

  if (loading) {                                                    // 加载中
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <ActivityIndicator size="large" />                          // 加载指示器
        <Text>加载中...</Text>                                       // 加载提示
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  if (error) {                                                      // 出错
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <Text>出错了:{error}</Text>                                 // 错误信息
        <Button title="重试" onPress={load} />                      // 重试按钮
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  return (                                                          // 成功
    <View style={styles.center}>                                    // 居中容器
      <Text style={styles.title}>{data.title}</Text>                // 显示标题
      <Text>{data.body}</Text>                                      // 显示正文
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },  // 居中容器样式
  title: { fontSize: 18, fontWeight: 'bold', marginBottom: 8 },     // 标题样式
});                                                                 // 结束样式表

这个例子展示了:data、loading、error 三态;try/catch/finally 处理成功、失败、收尾;res.ok 判断 HTTP 状态码;出错时提供重试按钮。

三态是数据获取的标准模式,几乎所有真实项目都会用到。

4.4 避免在已卸载组件上更新状态

如果用户在请求还没返回时离开了页面,请求返回后又调用了 setData,可能出问题。解决方法是:在 useEffect 里用一个标记,组件卸载时把它设为 true。

// JavaScript + React Native
useEffect(() => {                                                   // 注册副作用
  let cancelled = false;                                            // 定义取消标记

  async function load() {                                           // 定义异步加载函数
    try {                                                           // 开始 try
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
      const json = await res.json();                                // 解析 JSON
      if (!cancelled) setData(json);                                // 如果没卸载,更新 data
    } catch (e) {                                                   // 捕获错误
      if (!cancelled) setError(e.message);                          // 如果没卸载,更新 error
    } finally {                                                     // 收尾
      if (!cancelled) setLoading(false);                            // 如果没卸载,关闭 loading
    }                                                               // 结束 try
  }                                                                 // 结束 load

  load();                                                           // 调用 load

  return () => {                                                    // 返回清理函数
    cancelled = true;                                               // 组件卸载时设为 true
  };                                                                // 结束清理
}, []);                                                             // 空数组

组件卸载时,清理函数把 cancelled 设为 true。请求返回后,if (!cancelled) 为假,就不会再更新状态。

也可以使用 AbortController 直接取消请求:

// JavaScript + React Native
useEffect(() => {                                                   // 注册副作用
  const controller = new AbortController();                         // 创建控制器

  async function load() {                                           // 定义异步加载函数
    try {                                                           // 开始 try
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1', {  // 发请求
        signal: controller.signal,                                  // 传入信号
      });                                                           // 结束 fetch
      const json = await res.json();                                // 解析 JSON
      setData(json);                                                // 更新 data
    } catch (e) {                                                   // 捕获错误
      if (e.name !== 'AbortError') setError(e.message);             // 如果不是取消导致的错误
    } finally {                                                     // 收尾
      setLoading(false);                                            // 关闭 loading
    }                                                               // 结束 try
  }                                                                 // 结束 load

  load();                                                           // 调用 load

  return () => controller.abort();                                  // 组件卸载时取消请求
}, []);                                                             // 空数组

AbortController 更彻底,网络请求本身会被取消。


五、把数据渲染成列表

有了数据之后,通常要用 FlatList 渲染。这是第3课的知识,现在把它和数据获取结合起来。

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';  // 引入需要的组件

export default function App() {                                     // 定义并默认导出 App
  const [posts, setPosts] = useState([]);                           // 文章列表,初始空数组
  const [loading, setLoading] = useState(true);                     // 加载状态,初始 true
  const [error, setError] = useState(null);                         // 错误状态,初始 null

  useEffect(() => {                                                 // 注册副作用
    let cancelled = false;                                          // 取消标记

    async function load() {                                         // 定义异步加载函数
      try {                                                         // 开始 try
        const res = await fetch('https://jsonplaceholder.typicode.com/posts');  // 发请求
        const json = await res.json();                              // 解析 JSON
        if (!cancelled) setPosts(json);                             // 如果没卸载,更新 posts
      } catch (e) {                                                 // 捕获错误
        if (!cancelled) setError(e.message);                        // 如果没卸载,更新 error
      } finally {                                                   // 收尾
        if (!cancelled) setLoading(false);                          // 如果没卸载,关闭 loading
      }                                                             // 结束 try
    }                                                               // 结束 load

    load();                                                         // 调用 load

    return () => {                                                  // 返回清理函数
      cancelled = true;                                             // 卸载时设为 true
    };                                                              // 结束清理
  }, []);                                                           // 空数组

  if (loading) {                                                    // 加载中
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <ActivityIndicator size="large" />                          // 加载指示器
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  if (error) {                                                      // 出错
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <Text>加载失败:{error}</Text>                               // 错误信息
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  return (                                                          // 成功
    <FlatList                                                       // 高效列表
      data={posts}                                                  // 数据源
      keyExtractor={(item) => item.id.toString()}                   // 从每项提取唯一 key
      renderItem={({ item }) => (                                   // 如何渲染每一项
        <View style={styles.item}>                                  // 每项容器
          <Text style={styles.title}>{item.title}</Text>            // 显示标题
        </View>                                                     // 结束容器
      )}                                                            // 结束 renderItem
      ItemSeparatorComponent={() => <View style={styles.separator} />}  // 分隔线
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },  // 居中容器样式
  item: { padding: 16 },                                            // 列表项样式
  title: { fontSize: 16, fontWeight: 'bold' },                      // 标题样式
  separator: { height: 1, backgroundColor: '#eee' },                // 分隔线样式
});                                                                 // 结束样式表

useEffect 负责拉数据,loading、error 负责状态展示,FlatList 负责渲染数据。整个流程依然是"视图 = 函数(状态)":状态变,视图重新计算。


六、下拉刷新

FlatList 支持下拉刷新,配合数据获取非常自然。

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { FlatList, Text, RefreshControl } from 'react-native';      // 引入需要的组件

export default function App() {                                     // 定义并默认导出 App
  const [posts, setPosts] = useState([]);                           // 文章列表,初始空数组
  const [refreshing, setRefreshing] = useState(false);              // 刷新状态,初始 false

  const load = async () => {                                        // 定义加载函数
    const res = await fetch('https://jsonplaceholder.typicode.com/posts');  // 发请求
    const json = await res.json();                                  // 解析 JSON
    setPosts(json);                                                 // 更新 posts
  };                                                                // 结束 load

  useEffect(() => {                                                 // 注册副作用
    load();                                                         // 首次加载
  }, []);                                                           // 空数组

  const handleRefresh = async () => {                               // 定义下拉刷新函数
    setRefreshing(true);                                            // 打开 refreshing
    await load();                                                   // 重新加载
    setRefreshing(false);                                           // 关闭 refreshing
  };                                                                // 结束 handleRefresh

  return (                                                          // 返回界面描述
    <FlatList                                                       // 高效列表
      data={posts}                                                  // 数据源
      keyExtractor={(item) => item.id.toString()}                   // 从每项提取唯一 key
      renderItem={({ item }) => <Text>{item.title}</Text>}          // 渲染每一项
      refreshControl={                                              // 下拉刷新控件
        <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />  // 绑定状态和回调
      }                                                             // 结束 refreshControl
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

RefreshControl 是 FlatList 的 refreshControl 属性值。下拉时触发 onRefresh,更新 refreshing 状态,界面自动显示刷新指示器。


七、依赖数组常见问题

7.1 依赖不完整

// JavaScript + React Native
// 错误:使用了 count,却没有写进依赖数组
useEffect(() => {                                                   // 副作用回调
  console.log(count);                                               // 使用了 count
}, []);                                                             // 依赖数组是空的

useEffect 只在挂载时执行一次,count 后续变化不会触发它。如果逻辑上依赖 count,就必须写进依赖数组。

7.2 依赖里有对象或数组

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  // ...                                                          // 副作用内容
}, [{ id: 1 }]);                                                    // 每次渲染都创建新对象,引用不同

每次渲染都创建新对象,引用不同,useEffect 会反复执行。应该把对象拆成具体的基本类型值,比如 [id]。

7.3 无限循环

// JavaScript + React Native
// 错误:会无限循环
useEffect(() => {                                                   // 副作用回调
  setCount(count + 1);                                              // 更新 count
}, [count]);                                                        // 依赖 count

count 变化触发 useEffect,useEffect 又更新 count,无限循环。要避免在 useEffect 中更新它自己依赖的状态。

7.4 一个组件可以有多个 useEffect

一个组件里可以有多个 useEffect,每个负责一件独立的事。这是推荐做法。

// JavaScript + React Native
useEffect(() => {                                                   // 第一个副作用
  loadData();                                                       // 负责拉数据
}, []);                                                             // 空数组

useEffect(() => {                                                   // 第二个副作用
  const timer = setInterval(() => {}, 1000);                        // 负责定时器
  return () => clearInterval(timer);                                // 返回清理函数
}, []);                                                             // 空数组

分开写比混在一起更清晰,也更容易维护。


八、useEffect 与"视图 = 函数(状态)"

把 useEffect 放回"视图 = 函数(状态)"的框架里:

  • 渲染负责"根据状态算视图";
  • useEffect 负责"在渲染之后做副作用";
  • 副作用完成后,通过 setState 更新状态;
  • 状态变化,重新渲染,视图更新。
// 概念表达
渲染 -> 副作用 -> 更新状态 -> 重新渲染 -> 新视图                          // 完整闭环

副作用是"状态"的来源之一。它把外部世界的数据带进组件,再通过状态驱动界面。这就是为什么 useEffect 和"视图 = 函数(状态)"并不矛盾,而是它的补充。


九、练习题

练习题1:选择题——什么是副作用

题目:下面哪一项属于副作用?

A. 计算两个数字的和
B. 根据状态返回 JSX
C. 发起网络请求
D. 把 props 解构

参考答案:C

解读:副作用是指"计算返回值之外"的事情,比如网络请求、定时器、订阅、日志。计算、返回 JSX、解构都是纯计算,不算副作用。


练习题2:填空题——useEffect 执行时机

题目:补全下面的表格:

// 概念表达
不传依赖数组 -> 每次渲染后执行                                        // 不传时
传空数组 [] -> ____                                                  // 空数组
传 [count] -> ____                                                   // 具体依赖

参考答案:只在挂载后执行一次;挂载后执行一次,count 变化时再执行。

解读:依赖数组决定 useEffect 的执行时机。


练习题3:改错题——async 回调

题目:下面代码有一处错误,请指出并修正。

// JavaScript + React Native
useEffect(async () => {                                             // async 回调,不允许
  const res = await fetch('https://api.example.com');               // 发请求
  const json = await res.json();                                    // 解析 JSON
  setData(json);                                                    // 更新数据
}, []);                                                             // 空数组

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 普通回调
  async function load() {                                           // 内部定义 async 函数
    const res = await fetch('https://api.example.com');             // 发请求
    const json = await res.json();                                  // 解析 JSON
    setData(json);                                                  // 更新数据
  }                                                                 // 结束 load

  load();                                                           // 调用 load
}, []);                                                             // 空数组

解读:useEffect 的回调应该返回清理函数或 undefined。写成 async 会返回 Promise,React 无法识别。


练习题4:代码阅读题——执行顺序

题目:阅读代码,说出控制台打印顺序。

// JavaScript + React Native
function App() {                                                    // 定义组件
  console.log('渲染');                                              // 渲染时打印

  useEffect(() => {                                                 // 注册副作用
    console.log('副作用');                                          // 副作用执行时打印
  }, []);                                                           // 空数组

  return <Text>你好</Text>;                                         // 返回界面描述
}                                                                   // 结束组件

参考答案:先打印"渲染",后打印"副作用"。

解读:useEffect 在渲染完成之后执行。


练习题5:动手题——挂载时打印

题目:写一个 useEffect,让组件挂载时在控制台打印"组件已挂载"。

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log('组件已挂载');                                         // 打印日志
}, []);                                                             // 空数组

解读:空数组表示只在挂载后执行一次。


练习题6:简答题——三种依赖数组的区别

题目:简述 useEffect 不传依赖数组、传空数组、传具体依赖三种情况的区别。

参考答案:不传时每次渲染后都执行;传空数组时只在挂载后执行一次;传具体依赖时挂载后执行一次,依赖变化时再执行。

解读:依赖数组是控制副作用执行时机的开关。


练习题7:清理题——定时器

题目:写一个 useEffect,启动一个每秒打印一次的定时器,并在组件卸载时清除。

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  const timer = setInterval(() => {                                 // 启动定时器
    console.log('每秒一次');                                         // 每秒打印
  }, 1000);                                                         // 间隔 1000 毫秒

  return () => {                                                    // 返回清理函数
    clearInterval(timer);                                           // 清除定时器
  };                                                                // 结束清理函数
}, []);                                                             // 空数组

解读:定时器必须在组件卸载时清除,否则会持续运行,造成内存泄漏。


练习题8:数据获取题——fetch

题目:写一个 useEffect,组件挂载时请求 https://jsonplaceholder.typicode.com/posts/1,并把结果保存到 data 状态。

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  async function load() {                                           // 定义 async 函数
    const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
    const json = await res.json();                                  // 解析 JSON
    setData(json);                                                  // 更新 data
  }                                                                 // 结束 load

  load();                                                           // 调用 load
}, []);                                                             // 空数组

解读:在 useEffect 内部定义 async 函数并调用。空数组保证只执行一次。


练习题9:加载状态题

题目:在数据获取中加入 loading 状态,加载中显示"加载中…"。

参考答案:

// JavaScript + React Native
const [loading, setLoading] = useState(true);                       // 创建状态 loading,初始 true

useEffect(() => {                                                   // 副作用回调
  async function load() {                                           // 定义 async 函数
    const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
    const json = await res.json();                                  // 解析 JSON
    setData(json);                                                  // 更新 data
    setLoading(false);                                              // 关闭 loading
  }                                                                 // 结束 load

  load();                                                           // 调用 load
}, []);                                                             // 空数组

if (loading) return <Text>加载中...</Text>;                          // 加载中显示提示

解读:loading 初始为 true,请求完成后设为 false。界面根据 loading 显示不同内容。


练习题10:错误处理题

题目:在数据获取中加入 error 状态,出错时显示错误信息。

参考答案:

// JavaScript + React Native
const [error, setError] = useState(null);                           // 创建状态 error,初始 null

useEffect(() => {                                                   // 副作用回调
  async function load() {                                           // 定义 async 函数
    try {                                                           // 开始 try
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
      if (!res.ok) throw new Error(`请求失败:${res.status}`);      // 检查状态码
      const json = await res.json();                                // 解析 JSON
      setData(json);                                                // 更新 data
    } catch (e) {                                                   // 捕获错误
      setError(e.message);                                          // 更新 error
    }                                                               // 结束 try
  }                                                                 // 结束 load

  load();                                                           // 调用 load
}, []);                                                             // 空数组

if (error) return <Text>出错:{error}</Text>;                        // 出错时显示错误信息

解读:用 try/catch 捕获错误,用 error 状态保存错误信息。


练习题11:依赖项题——缺少依赖

题目:下面代码想监听 count 变化,但依赖数组不完整。请修正。

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log(count);                                               // 使用了 count
}, []);                                                             // 依赖数组是空的

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  console.log(count);                                               // 使用了 count
}, [count]);                                                        // 依赖 count

解读:useEffect 里使用了 count,就必须把 count 写进依赖数组。


练习题12:代码阅读题——无限循环

题目:下面代码为什么会导致无限循环?如何修正?

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  setCount(count + 1);                                              // 更新 count
}, [count]);                                                        // 依赖 count

参考答案:count 变化触发 useEffect,useEffect 又更新 count,形成循环。如果只是想初始化一次,应该用 [],或者避免在 useEffect 中更新它自己依赖的状态。

解读:依赖和它更新的状态存在循环依赖,就会无限循环。


练习题13:取消请求题

题目:写一个 useEffect,请求数据,并在组件卸载时取消请求(使用 cancelled 标记)。

参考答案:

// JavaScript + React Native
useEffect(() => {                                                   // 副作用回调
  let cancelled = false;                                            // 定义取消标记

  async function load() {                                           // 定义 async 函数
    try {                                                           // 开始 try
      const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');  // 发请求
      const json = await res.json();                                // 解析 JSON
      if (!cancelled) setData(json);                                // 如果没卸载,更新 data
    } catch (e) {                                                   // 捕获错误
      if (!cancelled) setError(e.message);                          // 如果没卸载,更新 error
    }                                                               // 结束 try
  }                                                                 // 结束 load

  load();                                                           // 调用 load

  return () => {                                                    // 返回清理函数
    cancelled = true;                                               // 组件卸载时设为 true
  };                                                                // 结束清理
}, []);                                                             // 空数组

解读:cancelled 是 useEffect 内部的局部变量。组件卸载时,清理函数把它设为 true,请求返回后判断 cancelled,避免在已卸载组件上更新状态。


练习题14:综合题——用户列表

题目:用 useEffect 请求 https://jsonplaceholder.typicode.com/users,用 FlatList 渲染用户列表,加载中显示加载指示器,出错显示错误信息。

参考答案:

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';  // 引入需要的组件

export default function App() {                                     // 定义并默认导出 App
  const [users, setUsers] = useState([]);                           // 用户列表,初始空数组
  const [loading, setLoading] = useState(true);                     // 加载状态,初始 true
  const [error, setError] = useState(null);                         // 错误状态,初始 null

  useEffect(() => {                                                 // 注册副作用
    let cancelled = false;                                          // 取消标记

    async function load() {                                         // 定义 async 函数
      try {                                                         // 开始 try
        const res = await fetch('https://jsonplaceholder.typicode.com/users');  // 发请求
        const json = await res.json();                              // 解析 JSON
        if (!cancelled) setUsers(json);                             // 如果没卸载,更新 users
      } catch (e) {                                                 // 捕获错误
        if (!cancelled) setError(e.message);                        // 如果没卸载,更新 error
      } finally {                                                   // 收尾
        if (!cancelled) setLoading(false);                          // 如果没卸载,关闭 loading
      }                                                             // 结束 try
    }                                                               // 结束 load

    load();                                                         // 调用 load

    return () => {                                                  // 返回清理函数
      cancelled = true;                                             // 卸载时设为 true
    };                                                              // 结束清理
  }, []);                                                           // 空数组

  if (loading) {                                                    // 加载中
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <ActivityIndicator size="large" />                          // 加载指示器
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  if (error) {                                                      // 出错
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <Text>加载失败:{error}</Text>                               // 错误信息
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  return (                                                          // 成功
    <FlatList                                                       // 高效列表
      data={users}                                                  // 数据源
      keyExtractor={(item) => item.id.toString()}                   // 从每项提取唯一 key
      renderItem={({ item }) => (                                   // 如何渲染每一项
        <View style={styles.item}>                                  // 每项容器
          <Text style={styles.name}>{item.name}</Text>              // 显示姓名
          <Text style={styles.email}>{item.email}</Text>            // 显示邮箱
        </View>                                                     // 结束容器
      )}                                                            // 结束 renderItem
      ItemSeparatorComponent={() => <View style={styles.separator} />}  // 分隔线
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },  // 居中容器样式
  item: { padding: 16 },                                            // 列表项样式
  name: { fontSize: 16, fontWeight: 'bold' },                       // 姓名样式
  email: { fontSize: 14, color: '#666' },                           // 邮箱样式
  separator: { height: 1, backgroundColor: '#eee' },                // 分隔线样式
});                                                                 // 结束样式表

解读:useEffect 负责数据获取,三态负责界面切换,FlatList 负责渲染列表。cancelled 防止在已卸载组件上更新状态。


练习题15:综合应用题——带刷新和重试的文章列表

题目:创建一个文章列表应用:

  1. 加载时显示"加载中…";
  2. 成功时用 FlatList 显示文章标题;
  3. 失败时显示错误信息和"重试"按钮;
  4. 支持下拉刷新。

参考答案:

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 useState 和 useEffect
import { View, Text, FlatList, ActivityIndicator, Button, RefreshControl, StyleSheet } from 'react-native';  // 引入需要的组件

export default function App() {                                     // 定义并默认导出 App
  const [posts, setPosts] = useState([]);                           // 文章列表,初始空数组
  const [loading, setLoading] = useState(true);                     // 加载状态,初始 true
  const [error, setError] = useState(null);                         // 错误状态,初始 null
  const [refreshing, setRefreshing] = useState(false);              // 刷新状态,初始 false

  const load = async () => {                                        // 定义加载函数
    try {                                                           // 开始 try
      setError(null);                                               // 清空 error
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');  // 发请求
      if (!res.ok) throw new Error(`请求失败:${res.status}`);      // 检查状态码
      const json = await res.json();                                // 解析 JSON
      setPosts(json);                                               // 更新 posts
    } catch (e) {                                                   // 捕获错误
      setError(e.message);                                          // 更新 error
    } finally {                                                     // 收尾
      setLoading(false);                                            // 关闭 loading
      setRefreshing(false);                                         // 关闭 refreshing
    }                                                               // 结束 try
  };                                                                // 结束 load

  useEffect(() => {                                                 // 注册副作用
    load();                                                         // 首次加载
  }, []);                                                           // 空数组

  const handleRefresh = () => {                                     // 定义下拉刷新函数
    setRefreshing(true);                                            // 打开 refreshing
    load();                                                         // 重新加载
  };                                                                // 结束 handleRefresh

  if (loading) {                                                    // 加载中
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <ActivityIndicator size="large" />                          // 加载指示器
        <Text>加载中...</Text>                                       // 加载提示
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  if (error) {                                                      // 出错
    return (                                                        // 返回界面描述
      <View style={styles.center}>                                  // 居中容器
        <Text>出错了:{error}</Text>                                 // 错误信息
        <Button                                                     // 重试按钮
          title="重试"                                              // 按钮文字
          onPress={() => {                                          // 点击回调
            setLoading(true);                                       // 打开 loading
            load();                                                 // 重新加载
          }}                                                        // 结束回调
        />                                                          // 结束 Button
      </View>                                                       // 结束容器
    );                                                              // 结束 return
  }                                                                 // 结束判断

  return (                                                          // 成功
    <FlatList                                                       // 高效列表
      data={posts}                                                  // 数据源
      keyExtractor={(item) => item.id.toString()}                   // 从每项提取唯一 key
      renderItem={({ item }) => (                                   // 如何渲染每一项
        <View style={styles.item}>                                  // 每项容器
          <Text style={styles.title}>{item.title}</Text>            // 显示标题
        </View>                                                     // 结束容器
      )}                                                            // 结束 renderItem
      ItemSeparatorComponent={() => <View style={styles.separator} />}  // 分隔线
      refreshControl={                                              // 下拉刷新控件
        <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />  // 绑定状态和回调
      }                                                             // 结束 refreshControl
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },  // 居中容器样式
  item: { padding: 16 },                                            // 列表项样式
  title: { fontSize: 16, fontWeight: 'bold' },                      // 标题样式
  separator: { height: 1, backgroundColor: '#eee' },                // 分隔线样式
});                                                                 // 结束样式表

解读:这是数据获取的完整形态。load 负责请求和状态更新;loading、error、posts、refreshing 四个状态分别管理不同场景;FlatList 负责渲染;RefreshControl 负责下拉刷新。所有状态变化都会触发重新渲染,界面自动切换。


十、总结

这一课我们学习了如何在 React Native 中处理副作用和数据获取。

核心要点:

  1. 组件渲染应该是纯的,只负责根据状态计算视图;
  2. 副作用放在 useEffect 中执行;
  3. useEffect 在渲染完成后执行;
  4. 依赖数组决定执行时机:不传、空数组、具体依赖;
  5. 清理函数用于善后,避免内存泄漏;
  6. 数据获取通常用 data、loading、error 三态管理;
  7. useEffect 回调不能直接写成 async,要在内部定义 async 函数;
  8. 用 cancelled 标记或 AbortController 避免在已卸载组件上更新状态;
  9. 获取到的数据可以用 FlatList 渲染;
  10. 下拉刷新用 RefreshControl;
  11. 一个组件里可以有多个 useEffect,每个负责一件独立的事。

记住一句话:

渲染负责算视图,副作用负责带数据;数据通过状态流回,状态驱动视图更新。


第5课预告

下一课,我们进入 【React Native娓娓道来】第5课 导航与多页面:用 React Navigation 组织页面流。

Logo

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

更多推荐