useEffect 是 React 函数组件中最核心、也最容易"似懂非懂"的 Hook 之一。
它的本质:
在组件渲染完成后,执行"副作用"逻辑。
所谓"副作用(Side Effect)",指的是:
- 请求接口
- 操作 DOM
- 订阅事件
- 定时器
- localStorage
- websocket
- 手动修改 document.title
- 等等......
一、先理解:为什么需要 useEffect?
React 组件本质应该是:
ini
UI = f(state)
即:
根据 state 渲染 UI
所以:
React 不希望你在 render 过程中做这些事情:
scss
fetch()
setTimeout()
addEventListener()
操作DOM
因为 render 应该是"纯函数"。
因此 React 提供:
scss
useEffect()
用于:
"渲染结束后,再执行副作用"
二、useEffect 基础语法
scss
useEffect(() => {
// 副作用逻辑
}, [依赖项])
它有两个参数:
| 参数 | 作用 |
|---|---|
| 第一个函数 | 副作用逻辑 |
| 第二个数组 | 控制什么时候执行 |
三、useEffect 执行时机(最重要)
1. 不写依赖数组
javascript
useEffect(() => {
console.log('effect')
})
等价于:
componentDidMount + componentDidUpdate
即:
每次组件渲染后都会执行
示例:
scss
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('执行了')
})
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
点击按钮:
执行了
执行了
执行了
因为:
- 初次渲染执行一次
- 每次更新再执行
2. 空依赖数组 []
scss
useEffect(() => {
console.log('只执行一次')
}, [])
等价于:
componentDidMount
即:
仅首次挂载执行一次
适合:
- 初始化
- 请求数据
- 注册事件
例如:
scss
useEffect(() => {
fetchUser()
}, [])
3. 有依赖项
scss
useEffect(() => {
console.log('count变化了')
}, [count])
意思:
首次执行 + count变化时执行
示例:
scss
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Tom')
useEffect(() => {
console.log('count effect')
}, [count])
return (
<>
<button onClick={() => setCount(count + 1)}>
count
</button>
<button onClick={() => setName('Jerry')}>
name
</button>
</>
)
}
结果:
- 修改 count → effect执行
- 修改 name → effect不执行
四、依赖数组底层原理
React 会:
浅比较依赖项是否变化
即:
vbnet
Object.is(旧值, 新值)
例如:
csharp
[count]
React 会比较:
ini
旧count === 新count ?
不同才重新执行 effect。
五、为什么对象会无限执行?
经典面试题。
错误写法:
scss
useEffect(() => {
console.log('执行')
}, [{ a: 1 }])
为什么死循环?
因为:
css
{ a: 1 } !== { a: 1 }
对象地址不同。
每次 render:
新对象
React 认为依赖变了。
正确做法:
使用:
useMemo
或者:
useRef
稳定引用。
六、副作用清理(cleanup)
这是 useEffect 最关键部分之一。
语法:
scss
useEffect(() => {
return () => {
// 清理逻辑
}
}, [])
return 的函数:
会在 effect 下一次执行前 或 组件卸载前执行
七、为什么需要清理?
因为:
很多副作用会"持续存在"。
例如:
- 定时器
- websocket
- 事件监听
- 订阅
如果不清理:
会:
- 内存泄漏
- 重复监听
- 多次触发
八、定时器清理
错误:
scss
useEffect(() => {
setInterval(() => {
console.log('hello')
}, 1000)
}, [])
问题:
组件卸载后:
定时器还在运行。
正确:
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('hello')
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
九、事件监听清理
错误:
scss
useEffect(() => {
window.addEventListener('resize', fn)
}, [])
问题:
组件卸载:
监听还在。
正确:
javascript
useEffect(() => {
window.addEventListener('resize', fn)
return () => {
window.removeEventListener('resize', fn)
}
}, [])
十、cleanup 执行时机(非常重要)
很多人理解错。
看代码:
javascript
useEffect(() => {
console.log('effect')
return () => {
console.log('cleanup')
}
}, [count])
当 count 更新:
执行顺序:
cleanup
effect
不是:
effect
cleanup
原因:
React 会先清理旧副作用,再执行新副作用。
十一、模拟生命周期
这是面试高频。
componentDidMount
scss
useEffect(() => {
console.log('mount')
}, [])
componentDidUpdate
javascript
useEffect(() => {
console.log('update')
})
或者:
scss
useEffect(() => {
console.log('count更新')
}, [count])
componentWillUnmount
javascript
useEffect(() => {
return () => {
console.log('卸载')
}
}, [])
十二、完整生命周期模拟
javascript
function App() {
useEffect(() => {
console.log('挂载')
return () => {
console.log('卸载')
}
}, [])
return <div>App</div>
}
十三、为什么 React18 会执行两次?
很多人遇到:
effect
cleanup
effect
以为自己写错了。
其实:
React18 StrictMode 故意这样。
目的是:
检测副作用是否安全
开发环境:
React 会:
挂载
卸载
再挂载
生产环境不会。
十四、useEffect 常见陷阱
1. 无限循环
错误:
scss
useEffect(() => {
setCount(count + 1)
}, [count])
因为:
count变
→ effect执行
→ setCount
→ count变
→ effect执行
死循环。
2. 闭包陷阱
scss
useEffect(() => {
setInterval(() => {
console.log(count)
}, 1000)
}, [])
这里永远打印初始值。
因为:
effect 只执行一次。
count 被闭包"锁住"。
解决:
依赖更新:
scss
useEffect(() => {
const timer = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(timer)
}, [count])
或者:
useRef
十五、useEffect 执行流程图
首次渲染:
render
↓
DOM更新
↓
effect执行
更新:
render
↓
DOM更新
↓
cleanup旧effect
↓
effect新执行
卸载:
cleanup执行
十六、useEffect vs useLayoutEffect
区别:
| Hook | 执行时机 |
|---|---|
| useEffect | 浏览器绘制后 |
| useLayoutEffect | DOM更新后、绘制前 |
useLayoutEffect 会阻塞渲染。
通常:
优先 useEffect。
只有:
- 测量DOM
- 防闪烁
- 动画同步
才用:
useLayoutEffect
十七、企业级最佳实践
1. effect 单一职责
不要:
scss
useEffect(() => {
请求接口
注册事件
设置标题
}, [])
应该拆开:
scss
useEffect(() => {
fetchData()
}, [])
useEffect(() => {
document.title = title
}, [title])
useEffect(() => {
window.addEventListener(...)
return () => ...
}, [])
2. 不要滥用 useEffect
很多逻辑:
根本不需要 effect。
错误:
scss
useEffect(() => {
setFullName(first + last)
}, [first, last])
正确:
ini
const fullName = first + last
因为:
能通过 render 推导出的值,不要存 state。
十八、最终理解(核心思想)
真正理解 useEffect:
不是背生命周期。
而是:
React 在同步 UI
useEffect 是:
当"某些数据变化后",同步外部世界。
比如:
| 数据变化 | 同步什么 |
|---|---|
| count变化 | 更新标题 |
| 用户登录 | 请求用户信息 |
| 页面显示 | 注册事件 |
| 页面销毁 | 清理资源 |
十九、一句话总结
ini
useEffect = 渲染后执行副作用
依赖数组:
控制副作用什么时候重新同步
cleanup:
清理旧副作用
二十、面试标准答案(建议背下来)
useEffect 用于在函数组件中处理副作用。
它会在组件渲染完成后执行。
第二个参数依赖数组用于控制 effect 的执行时机:
- 不传:每次渲染执行
-
\]:仅首次执行
effect 可以返回 cleanup 函数,用于:
- 清除定时器
- 取消订阅
- 移除事件监听
- 防止内存泄漏
React 在下一次 effect 执行前,会先执行 cleanup。