在 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
它们天然去重。
例如:
十二、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 双执行