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。

相关推荐
SimonKing1 小时前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
AskHarries1 小时前
MCP 基础:Server、Tool、Resource 和 Prompt
后端·程序员
Momo__1 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
无名氏同学1 小时前
React 16-19 新特性
react.js
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端