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

  • 不传:每次渲染执行
  • \]:仅首次执行

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

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

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

相关推荐
之歆1 小时前
DAY_18深度解析:数据类型转换与运算符全攻略(上)
前端·javascript
大家的林语冰1 小时前
pnpm 11 发布,弃用 JSON 和 npm CLI,进化为纯 ES6 模块,新增 pnpm pack-app 等命令,供应链保护默认启用,要求 Node
前端·javascript·node.js
漓漾li1 小时前
每日面试题-前端2
前端·react.js·面试
Alice-YUE2 小时前
深入解析 JS 事件循环:浏览器与 Node.js 的差异全解析
前端·javascript·笔记·学习
HYCS2 小时前
用pixijs实现fabricjs(二):对象的基础位置信息
前端·javascript·canvas
淸湫2 小时前
项目中使用了全局权限管理,请详细描述如何通过Vue Router的路由守卫来实现全局权限控制?
前端·vue.js
雪铃儿2 小时前
Shorebird 之外,Flutter Android 热更新还有什么选择
android·前端
李剑一2 小时前
前端必看 | Vue 刷新页面,生命周期钩子直接 "罢工",原来问题在这?90% 开发者都栽过!
前端·vue.js
閞杺哋笨小孩2 小时前
域名驱动多租户入驻:后台配置 + 前端解析
前端·vue.js