React18 函数组件执行顺序、严格模式下重复执行问题

在 React 中,函数组件的执行顺序和"为什么会执行两次"是很多人升级到 React18 后最容易困惑的问题之一。


一、React18 函数组件执行顺序

先看一个典型例子:

复制代码
import { useEffect, useLayoutEffect, useState } from 'react';

export default function App() {
  console.log('1. 组件函数执行');

  const [count, setCount] = useState(() => {
    console.log('2. useState 初始化');
    return 0;
  });

  useLayoutEffect(() => {
    console.log('3. useLayoutEffect');
  }, []);

  useEffect(() => {
    console.log('4. useEffect');
  }, []);

  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
    </div>
  );
}

正常模式下首次渲染顺序

执行结果:

复制代码
1. 组件函数执行
2. useState 初始化
3. useLayoutEffect
4. useEffect

二、React18 渲染阶段与提交阶段

React18 可以分成:

1. Render 阶段(渲染阶段)

这一阶段:

  • 执行函数组件

  • 执行 hooks

  • 生成 Virtual DOM

这里不能做副作用:

❌ 不要:

复制代码
fetch()
localStorage.setItem()
订阅事件

因为 React 可能:

  • 中断 render

  • 重试 render

  • 丢弃 render

所以 render 必须纯净。


2. Commit 阶段(提交阶段)

React 真正更新 DOM。

然后执行:

复制代码
useLayoutEffect
↓
浏览器绘制
↓
useEffect

三、useLayoutEffect 与 useEffect 顺序

useLayoutEffect

同步执行。

DOM 更新后立即执行,浏览器还没绘制。

适合:

  • DOM 测量

  • scroll

  • 动画定位


useEffect

异步执行。

浏览器绘制完成后执行。

适合:

  • 请求

  • 订阅

  • 日志


四、为什么 React18 会执行两次?

这是最核心的问题。


React18 StrictMode 开启后:

开发环境下:

复制代码
<React.StrictMode>
  <App />
</React.StrictMode>

React 会故意:

组件 mount → unmount → 再 mount

目的:

检查:

  • 副作用是否安全

  • cleanup 是否完整

  • 是否有不纯 render


五、React18 严格模式下真实执行顺序

示例:

复制代码
function App() {
  console.log('render');

  useEffect(() => {
    console.log('effect');

    return () => {
      console.log('cleanup');
    };
  }, []);

  return <div>hello</div>;
}

开发环境 StrictMode:

输出:

复制代码
render
render

effect
cleanup
effect

六、为什么 render 执行两次?

React18 在开发环境会:

Double Invoke(双调用)

包括:

  • 函数组件

  • useState initializer

  • useMemo

  • useReducer initializer

例如:

复制代码
const [state] = useState(() => {
  console.log('init');
  return 0;
});

会输出:

复制代码
init
init

七、哪些会重复执行?

会重复:

1. 函数组件

复制代码
function App() {}

2. useEffect

复制代码
useEffect(() => {})

会:

复制代码
执行
cleanup
再执行

3. useLayoutEffect

同样也会。


4. useState 初始化函数

复制代码
useState(() => {})

5. useMemo

复制代码
useMemo(() => {})

八、生产环境会不会?

不会。

严格模式双执行:

✅ 仅开发环境

❌ 生产环境不会

所以:

复制代码
npm run build

后不会重复。


九、为什么 React 要这样设计?

React18 引入:

Concurrent Rendering(并发渲染)

未来 React 可能:

  • 暂停 render

  • 恢复 render

  • 重试 render

  • 丢弃 render

所以 React 需要确保:

render 必须是"纯函数"

即:

相同输入 → 相同输出

不能有副作用。


十、最容易踩坑的地方


1. useEffect 发请求两次

例如:

复制代码
useEffect(() => {
  fetch('/api/user');
}, []);

开发环境会请求两次。


正确理解:

不是 bug。

是 StrictMode 检查副作用。


十一、如何避免请求两次?


方法1:接受它(推荐)

这是 React 官方推荐。

因为生产环境不会重复。


方法2:使用 ref 防重复

复制代码
const hasRequest = useRef(false);

useEffect(() => {
  if (hasRequest.current) return;

  hasRequest.current = true;

  fetchData();
}, []);

方法3:封装请求层缓存

例如:

  • SWR

  • React Query

它们天然去重。

例如:

TanStack Query 官方文档

SWR 官方文档


十二、React18 完整生命周期顺序(函数组件)

首次挂载:

复制代码
render
↓
生成 Virtual DOM
↓
DOM commit
↓
useLayoutEffect
↓
浏览器 paint
↓
useEffect

更新时:

复制代码
render
↓
diff
↓
commit
↓
cleanup(old effect)
↓
new effect

卸载时:

复制代码
cleanup

十三、StrictMode 下的核心记忆

开发环境:

复制代码
mount
↓
effect
↓
cleanup
↓
重新 mount
↓
effect

React 是故意模拟卸载重建。


十四、面试高频总结

React18 为什么组件执行两次?

答案:

React18 在开发环境下的 StrictMode 中,会故意对组件进行 mount → unmount → remount,用于检测副作用与不安全代码,为 Concurrent Rendering 做准备。


useEffect 为什么执行两次?

因为 StrictMode 会重新挂载组件,所以 effect 会重新执行一次。


生产环境会吗?

不会,仅开发环境。


十五、最佳实践


render 阶段不要做:

复制代码
fetch()
setTimeout()
addEventListener()
localStorage.setItem()

effect 中一定要 cleanup:

复制代码
useEffect(() => {
  const timer = setInterval(() => {}, 1000);

  return () => clearInterval(timer);
}, []);

推荐:

  • render 保持纯净

  • effect 做副作用

  • cleanup 做释放

  • 接受 StrictMode 双执行


十六、推荐官方资料

React 官方 StrictMode 文档

React 官方 useEffect 文档

React 官方渲染机制说明

相关推荐
LinXunFeng8 小时前
Obsidian - 使用 Share Note 分享笔记并自部署
前端·笔记·github
乘风gg12 小时前
为什么AI 时代来临,大部分人吃不到红利
前端·ai编程·claude
恋猫de小郭12 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
IT_陈寒13 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
恋猫de小郭13 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
Hyyy14 小时前
理解LLM的基本工作原理:预训练、微调、推理的区别
前端
Gatlin14 小时前
前端逆向与反逆向:一场猫鼠游戏的底层逻辑与实战
前端
代码煮茶14 小时前
React 组件封装方法论 —— 以 Todo App 为例
javascript·react.js
Pedantic14 小时前
本地通知(Local Notifications)学习笔记
前端
任沫15 小时前
Agent之Function Call
javascript·人工智能·go