useEffect 完整理解:依赖数组、副作用清理、模拟生命周期

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 的执行时机:

  • 不传:每次渲染执行
  • \[\]:仅首次执行
  • a:a变化时执行

effect 可以返回 cleanup 函数,用于:

  • 清除定时器
  • 取消订阅
  • 移除事件监听
  • 防止内存泄漏

React 在下一次 effect 执行前,会先执行 cleanup。

相关推荐
RobinDevNotes38 分钟前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神2 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神2 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat2 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss3 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真3 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment3 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么4 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模