useEffect
useEffect 依赖为空时 (无依赖),无论组件首次挂载还是重渲染,useEffect 都会执行
js
function Children(){
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Children rendered')
console.log(count) // 组件挂载时执行
})
return(
<div onClick={() => setCount(count + 1)}>children{count}</div>
)
}
useEffect 依赖依赖为空数组时 函数只会在组件首次挂载时执行,之后组件在渲染(在重新执行),useEffect中的函数不会再执行
useEffect 依赖为空数组时,只会在组件首次挂载时执行
js
function Children(){
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Children mounted') // 组件挂载时执行
},[])
return(
<div onClick={() => setCount(count + 1)}>children{count}</div>
)
}
useEffect 指定依赖项时 只有首次渲染和依赖项发生变化时才会执行
js
function Children(){
const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)
useEffect(() => {
console.log('Children rendered')
console.log(count) // 组件挂载时执行
},[count])
return(
<div>
<div onClick={() => setCount(count + 1)}>children{count}</div>
<div onClick={() => setCount2(count2 + 1)}>children{count2}</div>
</div>
)
}
每种useEffect 都 可以返回一个return函数,return函数在组件卸载时执行
js
function Children(){
const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)
useEffect(() => {
console.log('Children rendered')
console.log(count) // 组件挂载时执行
// 返回清理函数,组件卸载时执行
return () => {
console.log('Children unmounted')
}
})
return(
<div>
<div onClick={() => setCount(count + 1)}>children{count}</div>
<div onClick={() => setCount2(count2 + 1)}>children{count2}</div>
</div>
)
}
useEffect 用来干什么?
纯函数
纯函数,相同的输入,必然得到相同的结果。函数不依赖外部,也不影响外部。
🔹
useEffect用来执行那些"与渲染无关,但需要响应渲染结果的副作用操作"。
🧠 一、什么是"副作用(Side Effect)"
在 React 里,"副作用"指的是那些:
- 不会直接参与视图计算;
- 可能影响外部系统或环境 的操作。
简单说:
👉 渲染是"纯的"逻辑计算
👉 副作用是"脏的"真实世界交互
比如:
| 副作用类型 | 示例 |
|---|---|
| 网络请求 | 请求后端接口、上传文件 |
| DOM 操作 | 手动设置样式、聚焦 input |
| 计时器 | setTimeout、setInterval |
| 订阅 | WebSocket、事件监听 |
| 本地存储 | localStorage、sessionStorage |
| 日志 | console.log、打点统计 |
| 清理资源 | 组件卸载时取消订阅或清理计时器 |
⚙️ 二、为什么一定要放在 useEffect 里执行
React 希望渲染阶段是纯函数:
ini
UI = render(state, props)
意思是:
- 同样的 state、props → 一定返回相同的 UI;
- 不能有请求、修改全局变量、DOM 操作等副作用;
- 否则 React 在"并发渲染""重新渲染""中断渲染"时就会出现混乱。
而 useEffect 的回调是在:
✅ 真实 DOM 已经更新、浏览器完成绘制后 执行。
这就保证了:
- 副作用不会干扰渲染;
- 可以安全地访问真实 DOM;
- 能正确对应最新的渲染结果。
🌈 三、典型使用场景示例
1️⃣ 页面加载后发请求
scss
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
}, []); // 依赖空数组 -> 只在初次渲染后执行一次
2️⃣ 订阅事件(并在卸载时清理)
javascript
useEffect(() => {
const onResize = () => console.log(window.innerWidth);
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize); // cleanup
};
}, []);
3️⃣ 操作 DOM
scss
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus(); // 页面渲染后自动聚焦
}, []);
4️⃣ 设置计时器
javascript
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer); // 卸载时清理
}, []);
🔁 四、依赖数组的作用(再强调一下)
useEffect(() => {...}, [deps])
- 每次组件更新时,React 会检查依赖是否变化;
- 如果变化,才会重新执行副作用。
ini
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有 count 改变时执行
✅ 总结一句话
| 阶段 | 能做的事 | 工具 |
|---|---|---|
| 渲染阶段 | 计算 UI(不能有副作用) | 函数组件本身 |
| 提交阶段 | 真实 DOM 已更新,可以执行副作用 | useEffect |
useEffect用来获取数据
React 渲染函数组件的顺序大概是: 执行函数组件本身(App() 函数体) 计算 JSX → 返回虚拟 DOM(React elements) 在这个阶段,useEffect 只会被注册,不会立即执行回调 所以 console.log 不会立刻打印 React 根据虚拟 DOM 更新真实 DOM 浏览器渲染页面 渲染完成后(commit 阶段),React 执行 useEffect 回调 这时候 console.log('App mounted') 才会输出 如果它用来获取页面需要的数据,那岂不是一开始页面渲染的时候,没有数据
🧩 一、是的------第一次渲染时,确实没有数据!
用effect请求数据,组件第一次渲染时,是没有数据的。所以可以设置loading,当数据返回时再使用数据。如果数据返回错误,可以设置错误页面。
例如:
javascript
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
}, []);
return (
<div>
{user ? <p>{user.name}</p> : <p>Loading...</p>}
</div>
);
}
执行顺序:
1️⃣ 第一次渲染(Render 阶段)
- 执行
App() - 此时
user = null - 返回虚拟 DOM
<p>Loading...</p>
2️⃣ Commit 阶段 → 页面渲染到屏幕
- 用户立刻看到"Loading..."
- 页面已经有内容(不会空白)
3️⃣ React 执行 useEffect 回调
- 发起网络请求
/api/user
4️⃣ 请求返回 → setUser() 触发 re-render
- 再次执行
App() - 此时
user不为 null - 渲染
<p>{user.name}</p>
💡 二、为什么 React 要"先渲染再请求"
React 这么设计是刻意的,理由有三个:
✅ 1. 让页面尽快有内容(提升感知速度)
如果等待数据加载后再渲染,用户在慢网环境下就看到空白页面。
而现在:
即使数据没回来,也先渲染一个骨架屏 / loading 状态。
✅ 2. 避免阻塞渲染
假设在组件函数里直接请求:
csharp
const data = await fetch(...)
那整个函数必须等待网络完成后才继续执行,React 就没法继续渲染 UI 。
这会严重影响性能。
✅ 3. 副作用必须在渲染完成后执行
React 希望渲染是纯函数式的,也就是说:
"同样的输入(props/state)一定返回同样的 UI。"
如果你在渲染阶段发请求、改 DOM、操作浏览器,就打破了这种"纯粹性"。
所以所有副作用都被安排在 useEffect(渲染后执行)。
🧱 三、所以怎么办?
正确做法是:第一次渲染显示 Loading,拿到数据后再重新渲染。
javascript
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(r => r.json())
.then(setUser);
}, []);
if (!user) {
return <p>加载中...</p>;
}
return <p>你好,{user.name}</p>;
}
这就是 React 的常规数据加载模式。
React 认为:"即使没数据,也应该先渲染一个稳定的 UI 状态。"
useEffect是异步的
1️⃣ 核心原因
useEffect 是异步的,最核心的原因是:
保证渲染过程是纯函数、可中断、不阻塞浏览器绘制。
也就是说,React 希望 函数组件的渲染阶段(render)只做两件事:
- 接收 props 和 state
- 根据这些数据生成虚拟 DOM(VDOM)
不允许在渲染阶段做副作用(如 DOM 操作、网络请求、定时器等) ,因为这样会:
- 阻塞渲染,导致页面卡顿
- 破坏 React 可中断、可恢复的渲染能力
- 让函数组件变得不纯,难以预测输出
所以,React 选择把副作用延后到浏览器 绘制之后 执行,也就是异步执行。
2️⃣ 执行顺序示意
scss
render() // 函数组件执行,生成 VDOM
↓
DOM 更新 // React 把更新应用到真实 DOM
↓
浏览器绘制 // 用户看到页面
↓
useEffect 执行 // 执行副作用
这里的 "异步" 指的是 useEffect 的回调不会阻塞浏览器绘制。
3️⃣ 为什么不在 render 阶段执行?
假设我们在 render 阶段做副作用:
javascript
function App() {
const [count, setCount] = useState(0);
fetch("/api/data") // ❌ 副作用出现在 render
.then(res => console.log(res));
return <div>{count}</div>;
}
- 这个 fetch 会阻塞渲染吗?
如果 React 遇到多个状态更新,或者并发模式(Concurrent Mode),就可能多次渲染,副作用就会执行多次,产生 不可预测行为。 - React 渲染希望是 纯函数:相同输入一定产生相同输出,副作用会破坏这个原则。
4️⃣ useLayoutEffect 的"例外"
有些场景必须在 浏览器绘制前 执行副作用(如测量 DOM 尺寸、滚动条定位),React 提供了 useLayoutEffect:
- 同步执行
- 阻塞浏览器绘制
- 可以安全地读取或修改 DOM 布局
💡 总结
useEffect异步 → 先渲染再执行副作用,保证渲染纯净、流畅useLayoutEffect同步 → 绘制前执行副作用,保证布局正确
一句话记忆:
"useEffect:用户看见之后再做事;useLayoutEffect:用户看见之前先调整好一切。"