面试官:useEffect 为什么总背刺?我:闭包、ref 和依赖数组的三角恋

1 阅读4分钟

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 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页面绘制后执行
  • useLayoutEffectDOM 变更后、页面绘制前同步执行(可能会阻塞渲染)

一般推荐默认用 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”住你!