第八部分:第五节 - 生命周期与副作用 (`useEffect` Hook):组件的幕后工作
知识点: 组件生命周期(挂载 Mounting, 更新 Updating, 卸载 Unmounting - 高级概念),副作用 (Side Effects),useEffect
Hook (用于处理副作用,如数据获取、订阅、DOM 操作),useEffect
的依赖数组,useEffect
的清理函数。
讲解:
React 组件有自己的“生命周期”,从被创建并添加到页面上(挂载),到数据或状态变化导致重新渲染(更新),再到从页面上移除(卸载)。在这些生命周期的不同阶段,我们可能需要执行一些“副作用”操作。
副作用 (Side Effects):
副作用是指在组件渲染过程中不是直接计算结果的操作。常见的副作用包括:
- 数据获取 (Data Fetching): 从后端 API 请求数据。
- 订阅 (Subscriptions): 建立 WebSocket 连接,监听事件等。
- 手动修改 DOM: 直接操作浏览器 DOM (通常不推荐,除非必要)。
- 定时器 (Timers): 设置 setTimeout 或 setInterval。
- 日志记录 (Logging)。
这些操作不能直接放在组件函数体中(会无限循环或只执行一次),需要特殊的机制来管理。
useEffect
Hook:
在功能组件中,我们使用 useEffect
Hook 来处理副作用。useEffect
可以在组件渲染后执行代码。你可以把它想象成告诉 React:“在 DOM 更新后,帮我运行这段代码。”
useEffect
接收一个函数作为第一个参数,这个函数就包含了你要执行的副作用代码。
import React, { useState, useEffect } from 'react';function DataFetcher() {const [data, setData] = useState(null);const [isLoading, setIsLoading] = useState(true);// 使用 useEffect 处理数据获取副作用useEffect(() => {console.log('useEffect 运行了');// 在这里执行异步数据获取fetch('https://api.example.com/data') // 假设这是一个获取数据的 API.then(response => response.json()).then(data => {setData(data);setIsLoading(false);}).catch(error => {console.error('数据获取出错:', error);setIsLoading(false);});// useEffect 的默认行为是在每次渲染后都运行副作用函数// 这通常不是我们想要的,需要使用依赖数组控制执行时机}); // 没有依赖数组,每次渲染后都运行if (isLoading) {return <p>正在加载数据...</p>;}if (!data) {return <p>加载数据失败。</p>;}return (<div><h2>获取到的数据</h2><pre>{JSON.stringify(data, null, 2)}</pre></div>);
}
useEffect
的依赖数组 (Dependency Array):
useEffect
的第二个参数是一个依赖数组。它告诉 React 副作用应该在何时重新运行。
- 没有依赖数组: 副作用在每次渲染后都运行。
- 空数组
[]
: 副作用只在组件第一次挂载时运行一次 (componentDidMount 的替代)。适合执行一次性的设置或数据获取。 - 包含变量的数组
[variable1, variable2]
: 副作用在组件第一次挂载时运行一次,并且在依赖数组中的任何变量发生变化时重新运行。适合根据某些 State 或 Props 的变化来执行副作用。
// 只在组件挂载时获取一次数据 (空数组 [])
useEffect(() => {console.log('组件挂载,只运行一次');// 获取数据的代码...
}, []); // 空依赖数组// 当 userId 变化时重新获取数据
useEffect(() => {console.log('userId 或组件挂载时运行');if (userId) {// 根据新的 userId 获取用户详情fetch(`https://api.example.com/users/${userId}`).then(response => response.json()).then(userData => {// ... 更新用户 State});}
}, [userId]); // 依赖数组包含 userId// 没有依赖数组 (不推荐用于数据获取等异步操作,可能导致无限循环)
// useEffect(() => {
// console.log('每次渲染后都运行');
// });
useEffect
的清理函数 (Cleanup Function):
有时候,副作用需要在组件卸载时或下次副作用重新运行前进行清理,以避免内存泄漏或不必要的行为。比如清除定时器、取消订阅、关闭网络连接等。useEffect
可以选择返回一个函数,这个函数就是清理函数。
useEffect(() => {console.log('副作用设置');// 设置一个定时器const timerId = setInterval(() => {console.log('定时器运行中...');}, 1000);// 返回一个清理函数return () => {console.log('副作用清理');// 清除定时器clearInterval(timerId);};
}, []); // 空依赖数组,只在挂载时设置定时器,在卸载时清除
清理函数会在组件卸载时执行。如果 useEffect
因为依赖项变化而重新运行,清理函数会在新的副作用运行之前执行。
小结: useEffect
Hook 是功能组件中处理副作用的核心工具。通过依赖数组可以控制副作用的执行时机(挂载时、更新时、特定依赖变化时)。返回清理函数可以处理副作用的清理工作,避免资源泄漏。理解 useEffect
是掌握在 React 中进行异步操作和与外部系统交互的关键。
练习:
- 在你的
my-restaurant-app
项目中,修改MenuList
组件。 - 使用
useState
创建一个 State 变量menuItems
和isLoading
(初始为 true)。 - 使用
useEffect
Hook,在组件挂载时(空依赖数组),模拟从后端 API 获取菜单数据。你可以使用setTimeout
延迟几秒来模拟网络请求。 - 在模拟请求成功后,更新
menuItems
State,并将isLoading
设置为 false。 - 在组件的 JSX 中,使用条件渲染:如果
isLoading
为 true,显示“菜单加载中…”,否则渲染菜单列表 (menuItems.map(...)
)。 - (进阶)创建一个新的组件
Timer.jsx
。使用useState
和useEffect
来创建一个简单的计时器,每秒更新并显示当前时间。在useEffect
中设置setInterval
,并返回一个清理函数来清除定时器。