react 之 useState 和 useEffect 应用

文章目录

React函数式组件中,useStateuseEffect这两个核心钩子如何游离于生命周期(包括初始化、挂载、更新、卸载)等阶段

核心逻辑梳理

类组件有明确的生命周期方法(如constructorcomponentDidMountcomponentDidUpdatecomponentWillUnmount),而函数式组件本身没有这些"生命周期方法",而是通过:

  • 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:组件卸载

总结

  1. useState :核心是替代类组件的constructor(初始化状态)和setState(触发更新),无需处理this,更简洁;
  2. useEffect:通过「依赖数组」控制执行时机------空数组=挂载、指定依赖=更新、返回函数=卸载;
  3. 核心原则:函数式组件的生命周期是"声明式"的,无需记忆类组件的方法名,只需关注"依赖什么、何时执行、需要清理什么";
  4. 避坑点:依赖数组要完整(所有用到的状态/属性都要加),否则会导致逻辑执行异常;对象/数组依赖要保证引用稳定。

通过这两个钩子,你可以在函数式组件中实现类组件的所有生命周期逻辑,且代码更简洁、无this陷阱。

useState和useEffect的执行顺序-核心结论(先记重点)

useState同步执行 (组件函数体执行时立即执行),useEffect异步执行(组件DOM渲染/更新完成后执行),二者的执行顺序可总结为:

组件函数体执行 → 所有useState同步执行 → 组件DOM渲染/更新 → 所有useEffect按定义顺序异步执行(更新阶段会先执行旧副作用的清理函数)。

一、分阶段拆解执行顺序(附日志实例)

我们通过一个包含useStateuseEffect的完整组件,打印日志来直观验证执行顺序:

1. 挂载阶段(组件首次渲染)

执行流程
  1. 调用函数组件(执行函数体);
  2. useState定义顺序同步执行 所有useState(初始化状态,返回[状态值, 更新函数]);
  3. 执行函数体中剩余的同步代码(如变量定义、日志打印);
  4. React将组件DOM渲染到页面;
  5. 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. 更新阶段(状态/属性变化触发重渲染)

执行流程
  1. 状态/属性变化 → 组件函数体重新执行;
  2. useState定义顺序同步执行 所有useState(不再初始化,直接返回当前状态值和更新函数);
  3. 执行函数体剩余同步代码;
  4. React更新DOM;
  5. 对每个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移除)

执行流程
  1. 组件卸载时,按useEffect定义的反向顺序 执行所有useEffect的清理函数(先定义的后清理);
  2. 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:挂载/更新时按定义顺序执行,卸载时按定义反向顺序执行清理函数。

三、常见误区纠正

  1. ❌ 误区:useEffectuseState是同步执行的;

    ✅ 正解:useState同步(函数体执行时),useEffect异步(DOM渲染后)。

  2. ❌ 误区:更新阶段useState会重新执行初始化函数;

    ✅ 正解:useState的初始化函数仅挂载时执行一次,更新阶段仅返回当前状态值。

  3. ❌ 误区:所有useEffect在更新阶段都会执行;

    ✅ 正解:仅依赖项变化的useEffect会执行(清理旧副作用→执行新副作用)。


总结

  1. 核心顺序 :组件函数体执行(useState同步执行)→ DOM渲染/更新 → useEffect异步执行(更新阶段先清理旧副作用);
  2. 挂载阶段useState初始化→DOM渲染→useEffect副作用执行;
  3. 更新阶段useState返回当前值→DOM更新→依赖变化的useEffect先清理后执行新副作用;
  4. 执行规则useState/useEffect的执行顺序与定义顺序严格一致,useEffect永远在DOM操作后异步执行。
相关推荐
Jinuss1 小时前
源码分析之React中的Fiber节点介绍
前端·javascript·react.js
SJLoveIT1 小时前
xss攻击复习总结
前端·xss
集成显卡11 小时前
Bun v1.3.6 发布:内置 Tarball 归档支持、JSONC 解析、Bundle 分析增强等重磅更新!
javascript·新版本·bun.js
奔跑的web.11 小时前
TypeScript Enum 类型入门:从基础到实战
前端·javascript·typescript
盐真卿12 小时前
python2
java·前端·javascript
梦梦代码精12 小时前
BuildingAI vs Dify vs 扣子:三大开源智能体平台架构风格对比
开发语言·前端·数据库·后端·架构·开源·推荐算法
seabirdssss13 小时前
《bootstrap is not defined 导致“获取配置详情失败”?一次前端踩坑实录》
前端·bootstrap·html
kgduu13 小时前
js之表单
开发语言·前端·javascript
摘星编程14 小时前
React Native for OpenHarmony 实战:Picker 选择器组件详解
javascript·react native·react.js