文章目录
-
- React函数式组件中,`useState`和`useEffect`这两个核心钩子如何游离于生命周期(包括初始化、挂载、更新、卸载)等阶段
- 核心逻辑梳理
- 一、逐个拆解:生命周期的函数式实现(附实例)
-
- [1. 模拟`constructor`:useState初始化状态](#1. 模拟
constructor:useState初始化状态) -
- [类组件 vs 函数式组件对比](#类组件 vs 函数式组件对比)
- [2. 模拟`componentDidMount`:组件挂载(仅执行一次)](#2. 模拟
componentDidMount:组件挂载(仅执行一次)) - [3. 模拟`componentDidUpdate`:组件更新(依赖变化时执行)](#3. 模拟
componentDidUpdate:组件更新(依赖变化时执行)) - [4. 模拟`componentWillUnmount`:组件卸载(清理副作用)](#4. 模拟
componentWillUnmount:组件卸载(清理副作用)) - [5. 模拟"全生命周期":挂载+每次更新](#5. 模拟“全生命周期”:挂载+每次更新)
- [1. 模拟`constructor`:useState初始化状态](#1. 模拟
- 二、完整示例:函数式组件模拟类组件全生命周期
- useState和useEffect的执行顺序-核心结论(先记重点)
- 一、分阶段拆解执行顺序(附日志实例)
-
- [1. 挂载阶段(组件首次渲染)](#1. 挂载阶段(组件首次渲染))
- [2. 更新阶段(状态/属性变化触发重渲染)](#2. 更新阶段(状态/属性变化触发重渲染))
- [3. 卸载阶段(组件从DOM移除)](#3. 卸载阶段(组件从DOM移除))
- 二、关键细节补充(避坑点)
-
- [1. `useState`的执行时机](#1.
useState的执行时机) - [2. `useEffect`的异步特性](#2.
useEffect的异步特性) - [3. 多个`useState/useEffect`的顺序](#3. 多个
useState/useEffect的顺序)
- [1. `useState`的执行时机](#1.
- 三、常见误区纠正
React函数式组件中,useState和useEffect这两个核心钩子如何游离于生命周期(包括初始化、挂载、更新、卸载)等阶段
核心逻辑梳理
类组件有明确的生命周期方法(如constructor、componentDidMount、componentDidUpdate、componentWillUnmount),而函数式组件本身没有这些"生命周期方法",而是通过:
useState:替代类组件的constructor(初始化状态)和this.setState(触发更新);useEffect:通过「副作用函数 + 依赖数组」的组合,精准模拟挂载、更新、卸载所有生命周期行为。
先看核心对应关系
| 类组件生命周期阶段 | 函数式组件实现方式 | 核心作用 |
|---|---|---|
| constructor(初始化状态) | const [state, setState] = useState(初始值) |
初始化组件状态,无需构造函数 |
| componentDidMount(挂载) | useEffect(() => { ... }, []) |
组件挂载后仅执行一次 |
| componentDidUpdate(更新) | useEffect(() => { ... }, [依赖项1, 依赖项2]) |
依赖项变化时执行(模拟更新) |
| componentWillUnmount(卸载) | useEffect(() => { return () => { ... } }, []) |
返回的清理函数在卸载时执行 |
| 挂载+每次更新(无对应方法) | useEffect(() => { ... }) |
组件每次渲染后都执行 |
一、逐个拆解:生命周期的函数式实现(附实例)
1. 模拟constructor:useState初始化状态
类组件中constructor的核心作用是初始化状态 和绑定this,函数式组件中:
- 状态初始化:直接用
useState,无需构造函数; - 无
this绑定问题,事件函数直接定义即可。
类组件 vs 函数式组件对比
jsx
// 类组件:constructor初始化状态
class ClassComp extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }; // 初始化状态
}
// ...
}
// 函数式组件:useState初始化状态(替代constructor)
function FuncComp() {
// 初始化状态:count初始值0,setCount是更新函数
const [count, setCount] = useState(0);
// ...
}
2. 模拟componentDidMount:组件挂载(仅执行一次)
componentDidMount是组件挂载到DOM后执行的逻辑(如请求数据、初始化定时器、添加事件监听),函数式组件中用useEffect+空依赖数组实现。
实战实例:挂载后请求数据
jsx
import { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 模拟componentDidMount:挂载后请求数据(仅执行一次)
useEffect(() => {
// 异步请求用户数据
const fetchUser = async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await res.json();
setUser(data);
setLoading(false);
} catch (err) {
console.error('请求失败:', err);
setLoading(false);
}
};
fetchUser(); // 挂载后执行
}, []); // 空依赖数组 → 仅组件挂载时执行一次
if (loading) return <p>加载中...</p>;
if (!user) return <p>请求失败</p>;
return (
<div>
<h3>{user.name}</h3>
<p>邮箱:{user.email}</p>
<p>电话:{user.phone}</p>
</div>
);
}
关键解释
- 空依赖数组
[]是核心:告诉React"这个副作用仅在组件挂载时执行,后续更新不重复执行"; - 适合执行"一次性初始化逻辑"(如请求初始数据、绑定全局事件、启动定时器)。
3. 模拟componentDidUpdate:组件更新(依赖变化时执行)
componentDidUpdate是组件更新(状态/属性变化)后执行的逻辑,函数式组件中用useEffect+指定依赖数组实现------只有依赖项变化时,副作用才会重新执行。
实战实例:监听count变化(更新后执行逻辑)
jsx
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
// 模拟componentDidUpdate:仅count变化时执行
useEffect(() => {
console.log(`count更新为:${count}`);
// 比如更新DOM标题(仅count变化时更新)
document.title = `当前计数:${count}`;
}, [count]); // 依赖数组:仅count变化时触发
// 模拟componentDidUpdate:多个依赖项
useEffect(() => {
console.log(`count或name变化了:count=${count}, name=${name}`);
}, [count, name]); // 任意一个依赖变化就触发
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>count+1</button>
<br />
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="修改name"
/>
</div>
);
}
关键解释
- 依赖数组是"更新触发器":只有数组内的状态/属性变化,副作用才会执行;
- 对比类组件:无需手动判断
prevProps/prevState,React自动对比依赖项是否变化; - 注意:如果依赖项是对象/数组,要确保引用稳定(否则会频繁触发,可配合
useMemo/useCallback优化)。
4. 模拟componentWillUnmount:组件卸载(清理副作用)
componentWillUnmount是组件从DOM移除前执行的逻辑(如清理定时器、取消事件监听、取消网络请求),函数式组件中用useEffect的返回清理函数实现。
实战实例:清理定时器/事件监听
jsx
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 挂载时启动定时器
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 返回清理函数:模拟componentWillUnmount
return () => {
clearInterval(timer); // 卸载时清理定时器
console.log('组件卸载,定时器已清理');
};
}, []); // 空依赖 → 仅挂载/卸载时执行
return <p>已运行:{seconds} 秒</p>;
}
// 父组件:控制Timer的挂载/卸载
function TimerContainer() {
const [showTimer, setShowTimer] = useState(true);
return (
<div>
<button onClick={() => setShowTimer(!showTimer)}>
{showTimer ? '卸载定时器' : '挂载定时器'}
</button>
{showTimer && <Timer />}
</div>
);
}
关键解释
- 清理函数的执行时机:组件卸载时、副作用重新执行前(依赖项变化时);
- 必须清理的副作用:定时器、事件监听、网络请求取消、WebSocket连接关闭等,避免内存泄漏。
5. 模拟"全生命周期":挂载+每次更新
如果useEffect不写依赖数组 ,则副作用会在「组件挂载」和「每次更新」后执行(类组件无直接对应方法,等价于componentDidMount + componentDidUpdate)。
实例:每次渲染都执行的逻辑
jsx
function LogRender() {
const [count, setCount] = useState(0);
// 无依赖数组 → 挂载+每次更新都执行
useEffect(() => {
console.log('组件渲染/更新了,count=', count);
}); // 注意:无依赖数组
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count+1</button>
</div>
);
}
注意
- 这种写法容易导致性能问题(频繁执行),除非确实需要"每次渲染都执行",否则尽量指定依赖数组。
二、完整示例:函数式组件模拟类组件全生命周期
下面用一个完整例子,对比类组件和函数式组件的生命周期实现,让你直观理解:
jsx
// 类组件版本(全生命周期)
class ClassLifecycle extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
console.log('1. constructor:初始化状态');
}
componentDidMount() {
console.log('2. componentDidMount:组件挂载');
this.timer = setInterval(() => {
this.setState(prev => ({ count: prev.count + 1 }));
}, 1000);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log(`3. componentDidUpdate:count更新为${this.state.count}`);
}
}
componentWillUnmount() {
console.log('4. componentWillUnmount:组件卸载');
clearInterval(this.timer);
}
render() {
console.log('渲染组件');
return <p>Count: {this.state.count}</p>;
}
}
// 函数式组件版本(用useState+useEffect模拟)
function FuncLifecycle() {
const [count, setCount] = useState(0);
console.log('1. 初始化状态(替代constructor)+ 渲染组件');
useEffect(() => {
console.log('2. 模拟componentDidMount:组件挂载');
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 模拟componentWillUnmount:清理副作用
return () => {
console.log('4. 模拟componentWillUnmount:组件卸载');
clearInterval(timer);
};
}, []); // 空依赖 → 仅挂载/卸载
useEffect(() => {
console.log(`3. 模拟componentDidUpdate:count更新为${count}`);
}, [count]); // 依赖count → 仅count更新时执行
return <p>Count: {count}</p>;
}
执行日志对比(完全一致)
javascript
// 挂载时
1. constructor:初始化状态 / 1. 初始化状态+渲染组件
渲染组件
2. componentDidMount:组件挂载 / 2. 模拟componentDidMount:组件挂载
// count更新时
渲染组件
3. componentDidUpdate:count更新为1 / 3. 模拟componentDidUpdate:count更新为1
// 卸载时
4. componentWillUnmount:组件卸载 / 4. 模拟componentWillUnmount:组件卸载
总结
- useState :核心是替代类组件的
constructor(初始化状态)和setState(触发更新),无需处理this,更简洁; - useEffect:通过「依赖数组」控制执行时机------空数组=挂载、指定依赖=更新、返回函数=卸载;
- 核心原则:函数式组件的生命周期是"声明式"的,无需记忆类组件的方法名,只需关注"依赖什么、何时执行、需要清理什么";
- 避坑点:依赖数组要完整(所有用到的状态/属性都要加),否则会导致逻辑执行异常;对象/数组依赖要保证引用稳定。
通过这两个钩子,你可以在函数式组件中实现类组件的所有生命周期逻辑,且代码更简洁、无this陷阱。
useState和useEffect的执行顺序-核心结论(先记重点)
useState 是同步执行 (组件函数体执行时立即执行),useEffect 是异步执行(组件DOM渲染/更新完成后执行),二者的执行顺序可总结为:
组件函数体执行 → 所有
useState同步执行 → 组件DOM渲染/更新 → 所有useEffect按定义顺序异步执行(更新阶段会先执行旧副作用的清理函数)。
一、分阶段拆解执行顺序(附日志实例)
我们通过一个包含useState和useEffect的完整组件,打印日志来直观验证执行顺序:
1. 挂载阶段(组件首次渲染)
执行流程
- 调用函数组件(执行函数体);
- 按
useState定义顺序同步执行 所有useState(初始化状态,返回[状态值, 更新函数]); - 执行函数体中剩余的同步代码(如变量定义、日志打印);
- React将组件DOM渲染到页面;
- 按
useEffect定义顺序异步执行 所有useEffect的副作用函数。
代码实例(挂载阶段)
jsx
import { useState, useEffect } from 'react';
function OrderDemo() {
console.log('1. 开始执行组件函数体');
// 第一个useState:同步执行
const [count, setCount] = useState(() => {
console.log('2. useState(count) 初始化执行');
return 0;
});
// 第二个useState:同步执行
const [name, setName] = useState(() => {
console.log('3. useState(name) 初始化执行');
return 'React';
});
console.log('4. 组件函数体同步代码执行完成(count=', count, 'name=', name, ')');
// 第一个useEffect:DOM渲染后异步执行
useEffect(() => {
console.log('5. 第一个useEffect 副作用执行(挂载)');
return () => {
console.log('第一个useEffect 清理函数执行');
};
}, []);
// 第二个useEffect:DOM渲染后异步执行
useEffect(() => {
console.log('6. 第二个useEffect 副作用执行(挂载,依赖count)');
return () => {
console.log('第二个useEffect 清理函数执行');
};
}, [count]);
return (
<div>
<p>count: {count}</p>
<p>name: {name}</p>
<button onClick={() => setCount(count + 1)}>count+1</button>
<button onClick={() => setName('Vue')}>修改name</button>
</div>
);
}
export default OrderDemo;
挂载阶段日志输出(顺序固定)
1. 开始执行组件函数体
2. useState(count) 初始化执行
3. useState(name) 初始化执行
4. 组件函数体同步代码执行完成(count= 0 name= React )
5. 第一个useEffect 副作用执行(挂载)
6. 第二个useEffect 副作用执行(挂载,依赖count)
2. 更新阶段(状态/属性变化触发重渲染)
执行流程
- 状态/属性变化 → 组件函数体重新执行;
- 按
useState定义顺序同步执行 所有useState(不再初始化,直接返回当前状态值和更新函数); - 执行函数体剩余同步代码;
- React更新DOM;
- 对每个
useEffect:- 若依赖项变化 → 先执行上一次的清理函数 (卸载旧副作用),再执行新的副作用函数;
- 若依赖项未变化 → 不执行任何操作。
操作:点击"count+1"按钮(触发count更新)
更新阶段日志输出
1. 开始执行组件函数体
2. useState(count) 初始化执行 (注意:这里不再执行初始化函数,仅返回当前值!实际日志不会打印这行,因为useState的初始化函数仅挂载时执行)
3. useState(name) 初始化执行 (同理,仅挂载时执行)
4. 组件函数体同步代码执行完成(count= 1 name= React )
第二个useEffect 清理函数执行 (先清理旧的副作用)
6. 第二个useEffect 副作用执行(挂载,依赖count) (执行新的副作用)
关键说明
useState的初始化函数(如useState(() => { ... }))仅挂载时执行一次,更新阶段只会返回当前状态值,不会重新执行初始化逻辑;- 只有依赖项变化的
useEffect才会执行(如上例中count变化,第二个useEffect执行,第一个useEffect因空依赖不执行); - 清理函数先执行,再执行新的副作用函数(避免旧副作用与新状态冲突)。
3. 卸载阶段(组件从DOM移除)
执行流程
- 组件卸载时,按
useEffect定义的反向顺序 执行所有useEffect的清理函数(先定义的后清理); useState无卸载逻辑,状态直接销毁。
操作:移除组件(如父组件控制显示隐藏)
卸载阶段日志输出
第二个useEffect 清理函数执行
第一个useEffect 清理函数执行
二、关键细节补充(避坑点)
1. useState的执行时机
- 始终在组件函数体执行时同步执行 ,且执行顺序与定义顺序严格一致(React靠顺序识别每个
useState,不能在条件语句中定义useState); - 即使多次调用
setState,组件也只会重渲染一次(React会批量更新状态),但useState仍会在重渲染时同步执行。
2. useEffect的异步特性
useEffect的副作用函数永远在DOM渲染/更新完成后执行,不会阻塞页面渲染(这是React设计的核心,避免副作用阻塞UI);- 可以理解为:
useEffect是"渲染后的回调",而useState是"渲染中的状态准备"。
3. 多个useState/useEffect的顺序
useState:按定义顺序执行,React通过"调用顺序"而非变量名识别每个状态(比如不能写if (count > 0) { useState(1) });useEffect:挂载/更新时按定义顺序执行,卸载时按定义反向顺序执行清理函数。
三、常见误区纠正
-
❌ 误区:
useEffect和useState是同步执行的;✅ 正解:
useState同步(函数体执行时),useEffect异步(DOM渲染后)。 -
❌ 误区:更新阶段
useState会重新执行初始化函数;✅ 正解:
useState的初始化函数仅挂载时执行一次,更新阶段仅返回当前状态值。 -
❌ 误区:所有
useEffect在更新阶段都会执行;✅ 正解:仅依赖项变化的
useEffect会执行(清理旧副作用→执行新副作用)。
总结
- 核心顺序 :组件函数体执行(
useState同步执行)→ DOM渲染/更新 →useEffect异步执行(更新阶段先清理旧副作用); - 挂载阶段 :
useState初始化→DOM渲染→useEffect副作用执行; - 更新阶段 :
useState返回当前值→DOM更新→依赖变化的useEffect先清理后执行新副作用; - 执行规则 :
useState/useEffect的执行顺序与定义顺序严格一致,useEffect永远在DOM操作后异步执行。