🧠 系列前言:
面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!
💬 面试官发问:
“说说你对
useEffect
的理解?依赖项为什么总是填不对?闭包陷阱怎么解?”
哎哟妈呀,这题一出,多少前端人梦回凌晨 2 点 debug 页面逻辑,满脸问号:我明明写对了,怎么又触发了?
🎯 快答区(面霸速记版)
useEffect
是一个副作用钩子,默认在组件渲染后执行- 依赖项数组控制副作用的触发时机
- 如果你不理解闭包和引用变化,
useEffect
就会变身背刺小王子 - React 的规则是:只要依赖项变了就重新执行
所以填错依赖数组 = 自找 bug
🧬 useEffect 的爱恨情仇
🪝 一、useEffect
到底干嘛的?
在类组件中我们有:
componentDidMount() // 初始化执行一次
componentDidUpdate() // 每次更新都执行
componentWillUnmount() // 组件卸载时执行清理
而在函数组件里,一个 useEffect
全包了:
useEffect(() => {
console.log('副作用逻辑来咯~')
return () => {
console.log('组件卸载 or 依赖变化,清理啦!')
}
}, [依赖项])
你可以认为:
useEffect = didMount + didUpdate + willUnmount
的组合技。
🎭 二、为什么依赖项这么重要?
你写副作用:
useEffect(() => {
fetchData(keyword)
}, [])
看起来没毛病吧?但 keyword
改了,页面没更新,debug 一看:
啊这……你把
keyword
忘写进依赖数组了!
React 的机制是:
只要依赖数组里的值发生变化,
useEffect
就重新执行。
而且还有 ESLint 小助手在旁边耳语:
“你漏了依赖项,要不要加上?”
别不信邪,真不加,等着被 bug 追着打。
🧟♂️ 三、闭包 + useEffect = 鬼打墙现场
来看经典误区:
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 👈 永远是 0!
}, 1000)
}, [])
你以为能打印 count 的实时值?结果它永远是 0。为啥?
闭包记住的是第一次的 count 值,后续不会变。
React 不会每次都重新创建这个函数,它只在第一次 []
时执行了一次副作用。
✅ 正确解法 1:依赖更新版本
useEffect(() => {
const timer = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(timer)
}, [count]) // 每次 count 变化都重新注册
但这样每次都清除+重新 setInterval,其实效率不高。
✅ 正确解法 2:使用 ref 保存可变值
const countRef = useRef(count)
useEffect(() => {
countRef.current = count
}, [count])
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current) // 永远拿到最新值
}, 1000)
return () => clearInterval(timer)
}, [])
完美解决闭包问题,让副作用逻辑始终拿到最新值。
🕳 四、useEffect 执行时机:同步还是异步?
很多人以为 useEffect
是异步的,其实更准确地说是:
useEffect
是 在浏览器完成 paint 之后 执行的副作用,也就是 非阻塞渲染
🎥 补充一个:
useEffect
:页面绘制后执行useLayoutEffect
:DOM 变更后、页面绘制前同步执行(可能会阻塞渲染)
一般推荐默认用 useEffect
,只有你要测量 DOM 或强制修改布局时,才上 useLayoutEffect
。
🎯 五、React 官方建议怎么写依赖项?
✅ 尽可能声明清晰依赖
useEffect(() => {
fetchData(keyword)
}, [keyword])
❌ 不推荐写成这样:
useEffect(() => {
fetchData(keyword)
}, []) // 靠闭包?你会后悔的
✅ 对象依赖,记得 memo
const filter = useMemo(() => ({ name }), [name])
useEffect(() => {
fetchData(filter)
}, [filter])
避免每次都触发,因为 { name }
每次都是新对象。
🎓 装 X 语录(限时使用)
“useEffect 的本质是响应式副作用收集器,依赖数组的变化驱动副作用重跑。”
“闭包陷阱其实是 JS 的机制,不是 React 的锅。ref 是解决数据脱离组件周期的利器。”
“副作用的清理逻辑相当于生命周期中的 willUnmount,能防止内存泄漏和状态污染。”
说完记得压低语气、语速慢一点,表现你是“老油条 + 热爱原理派”。
✅ 总结一句话
useEffect = 渲染之后的副作用管理器,依赖数组驱动重跑,闭包问题靠 ref 或更新依赖解决
。
写对它,你是高手;写错它,它就是你项目里的定时炸弹💣。
🔮 明日预告
明天我们聊聊 useCallback
和 useMemo
,它们到底是性能优化神器,还是“性能幻想剂”?怎么用才能不白费 CPU?⚙️
📌 点赞 + 收藏 + 关注系列,React Hook 不再“Hook”住你!