React: useEffect 与生命周期

最后更新:2026-10-26

如果说 useState 是组件的"记忆",那 useEffect 就是组件的"照相机"——它让你在组件渲染到屏幕后("拍完照")去做一些额外的事情:请求数据、订阅事件、操作 DOM、设置定时器。


1. 你将学到



2. 一个数据加载页面的故事

(1) 痛点:数据请求导致无限循环

Alice 想在组件加载时请求用户数据,她这样写:

JSX
function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null)

  // ❌ 错误:在组件函数体中直接请求数据
  fetch('/api/users/' + userId)
    .then(res => res.json())
    .then(data => setUser(data))

  return <div>{user?.name}</div>
}
▶ 试一试

问题:

  1. fetch → setUser(data) → 组件重新渲染
  2. 重新渲染 → 又执行 fetch → 又设置状态 → 又渲染...
  3. 无限循环! 页面卡死

Alice 需要一种方式:"只在组件挂载时执行一次"。

(2) useEffect 的解法

JSX
import { useState, useEffect } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  // ✅ 正确:useEffect + 空依赖数组
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(res => {
        if (!res.ok) throw new Error('加载失败')
        return res.json()
      })
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err.message)
        setLoading(false)
      })
  }, [userId])  // 只在 userId 变化时重新执行

  if (loading) return <p>加载中...</p>
  if (error) return <p>错误:{error}</p>
  return <h1>{user.name}</h1>
}
▶ 试一试

收益:useEffect 让副作用和渲染逻辑分离——渲染只负责 UI,副作用在渲染完成后执行。



3. useEffect 基础

(1) 基本语法

JSX
useEffect(() => {
  // 副作用逻辑(渲染完成后执行)
  console.log('组件渲染完成!')
  
  return () => {
    // 清理函数(组件卸载或重新执行前运行)
    console.log('清理副作用')
  }
}, [依赖数组])
▶ 试一试
部分 说明
() => { ... } 副作用回调,在渲染提交到屏幕后执行
return () => { ... } 清理函数,组件卸载或依赖变化重新执行前运行
[依赖] 控制 useEffect 何时重新执行的数组
100%
graph TB
    A[组件渲染] --> B[React 提交 DOM 更新]
    B --> C[useEffect 回调执行]
    C --> D{依赖变化?}
    D -->|是| E[执行上一个清理函数]
    E --> F[重新执行 useEffect]
    D -->|否| G[跳过]
    F --> H{组件卸载?}
    G --> H
    H -->|是| I[执行清理函数]
    
    style C fill:#61dafb,color:#000
    style I fill:#ff6b6b,color:#fff

(2) 依赖数组的三种模式

JSX
// 模式 1:空数组 [] — 只在挂载时执行一次
useEffect(() => {
  console.log('组件挂载')
  fetch('/api/init').then(setData)
}, [])  // 永不重新执行

// 模式 2:有依赖 [a, b] — 依赖变化时重新执行
useEffect(() => {
  console.log('userId 变化了:', userId)
  fetch('/api/users/' + userId).then(setUser)
}, [userId])  // userId 变化时重新执行

// 模式 3:没有依赖数组 — 每次渲染都执行
useEffect(() => {
  console.log('每次渲染都执行')
})  // 注意:没有数组!
▶ 试一试
依赖数组 执行时机 使用场景
[] 仅挂载时一次 初始化数据请求、订阅
[dep1, dep2] 挂载时 + 依赖变化时 数据随参数变化
无数组 每次渲染后 极少用(通常用别的方式替代)


4. 清理函数

清理函数是防止内存泄漏和状态竞争的关键。

(1) 定时器的清理

▶ 试一试

(2) 事件监听的清理

▶ 试一试

(3) 请求取消(避免状态竞争)

JSX
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    let cancelled = false

    fetch('/api/search?q=' + query)
      .then(res => res.json())
      .then(data => {
        // ✅ 如果组件已卸载或 query 已变,不更新状态
        if (!cancelled) {
          setResults(data)
        }
      })

    // ✅ 清理:标记取消
    return () => {
      cancelled = true
    }
  }, [query])

  return <div>{results.map(r => <p key={r.id}>{r.name}</p>)}</div>
}
// 用户快速切换搜索词时:
// 1. 搜索"A" → useEffect 执行
// 2. 搜索"AB" → 清理函数标记 cancelled=true → useEffect 重新执行
// 3. 搜索"A" 的响应回来 → cancelled=true → 不更新状态(避免闪现) 
▶ 试一试

输出:

TEXT
副标题:"useEffect Life Cycle Demonstration"。状态:count、logs。通过 useEffect 管理副作用。定时器更新

▶ 示例:清理函数综合演示

▶ 试一试

输出:

TEXT
计数每 2 秒自动 +1,空格键 +5。日志:"⏱️ Timer started"。卸载 → "⏱️ Timer cleared"、"⌨️ Keyboard listening removed"


5. useEffect 与组件生命周期的映射

类组件时代有 3 个生命周期钩子,Hooks 时代的 useEffect 一个顶三个:

100%
graph LR
    subgraph "类组件的生命周期"
        A1[componentDidMount] --> A2[componentDidUpdate]
        A2 --> A3[componentWillUnmount]
    end
    
    subgraph "Hooks 的 useEffect"
        B1["useEffect(fn, [])<br/>挂载时执行"] 
        B2["useEffect(fn, [dep])<br/>挂载 + 依赖更新时执行"]
        B3["return cleanup<br/>卸载时或重新执行前清理"]
    end

输出:

TEXT
副标题:"useEffect Life Cycle Demonstration"。显示:"useEffect Life Cycle Demonstration"。状态:count(setter: setCount)、logs(setter: setLogs)。useEffect 管理副作用。基于定时器的更新
类组件生命周期 对应的 useEffect
componentDidMount useEffect(() => { ... }, [])
componentDidUpdate useEffect(() => { ... })(无数组)或 useEffect(() => { ... }, [dep])
componentWillUnmount useEffect(() => { return () => { ... } }, []) 中的清理函数

▶ 示例:单个 useEffect 完成多个生命周期

JSX
// ============================================
// 示例:聊天室组件——一个 useEffect 覆盖全部生命周期
// ============================================

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // === componentDidMount + componentDidUpdate ===
    console.log(`🟢 连接到聊天室:${roomId}`)
    const connection = createConnection(roomId)
    connection.connect()

    connection.on('message', (msg) => {
      setMessages(prev => [...prev, msg])
    })

    // === componentWillUnmount + 重新执行前清理 ===
    return () => {
      console.log(`🔴 断开聊天室:${roomId}`)
      connection.disconnect()
    }
  }, [roomId])  // roomId 变化时:断开旧连接 → 连接新聊天室

  return (
    <div>
      <h3>聊天室:{roomId}</h3>
      {messages.map((msg, i) => <p key={i}>{msg}</p>}
    </div>
  )
}

// 使用
<ChatRoom roomId="general" />     // 连接 general 聊天室
<ChatRoom roomId="random" />      // roomId 变化 → 断开 general → 连接 random
// 组件卸载时 → 断开当前连接(清理函数执行)
▶ 试一试

输出:

TEXT
挂载:console "🟢 Connected to chat room: general"。切换 roomId:"🔴 Left: general" → "🟢 Connected: random"。卸载:断开连接。


6. useEffect vs useLayoutEffect

React 提供了两个"副作用"Hook,区别在于执行时机。

100%
graph LR
    A[触发渲染] --> B[计算虚拟 DOM]
    B --> C[DOM 更新]
    C --> D{useLayoutEffect?}
    D -->|是| E[同步执行<br/>阻塞浏览器绘制]
    D -->|否| F[浏览器绘制完成]
    F --> G[useEffect 异步执行]
    
    style E fill:#faad14,color:#000
    style G fill:#52c41a,color:#fff
Hook 执行时机 阻塞绘制 适用场景
useEffect 浏览器绘制之后(异步) ❌ 90% 的场景:数据请求、事件监听、日志
useLayoutEffect DOM 更新后、浏览器绘制之前(同步) ✅ 测量 DOM 尺寸、读取布局、同步动画
▶ 试一试

规则:先写 useEffect,只有遇到"闪烁"或"布局抖动"问题才换成 useLayoutEffect。



7. 完整示例:数据仪表盘

▶ 展开运行示例(98 行)
▶ 试一试

输出:

TEXT
🟢 连接到聊天室:...
🔴 断开聊天室:...

预期输出:数据仪表盘,含 4 个统计卡片、实时刷新开关、窗口大小显示、最后更新时间。


▶ 示例 3:useEffect 实现实时数据同步

JSX
function LiveSearch({ apiBase }) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    if (!query.trim()) { setResults([]); return }
    setLoading(true)
    const controller = new AbortController()
    fetch(`${apiBase}/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => { setResults(data); setLoading(false) })
      .catch(err => { if (err.name !== 'AbortError') setLoading(false) })
    return () => controller.abort()
  }, [query, apiBase])

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <input value={query} onChange={e => setQuery(e.target.value)}
        placeholder="Search users..." style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
      {loading && <p>Searching...</p>}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {results.map(r => <li key={r.id} style={{ padding: 4, borderBottom: '1px solid #f0f0f0' }}>{r.name}</li>)}
      </ul>
    </div>
  )
}
▶ 试一试

输出:

TEXT
useEffect 管理副作用

输出:

TEXT
显示:"Searching..."。状态:query(setter: setQuery)、results(setter: setResults)、loading(setter: setLoading)。输入框:Search users...。列表:{r.name}。useEffect 管理副作用。异步数据获取/加载状态

▶ 示例 4:useEffect 监听窗口尺寸变化

▶ 试一试

输出:

TEXT
useEffect 管理副作用

输出:

TEXT
显示:"📱 Single column layout"。状态:size(setter: setSize)。useEffect 管理副作用

▶ 示例 5:useEffect + localStorage 持久化状态

▶ 试一试

输出:

TEXT
待办列表保存到 localStorage。添加任务,点击切换(删除线)。刷新页面 → 任务保留。按回车添加。

❓ 常见问题

Q useEffect 和直接在组件里写代码有什么区别?
A useEffect 的代码在渲染完成后才执行,不会阻塞 UI。直接在组件函数体里写的代码在渲染过程中执行(会阻塞渲染),而且每次渲染都会执行。最关键的区别是:useEffect 可以在组件卸载时做清理。
Q 为什么 useEffect 的清理函数会在每次重新执行前运行?
A 这是 React 的设计保证——防止因依赖变化导致的"竞争状态"。比如依赖 userId 变化时,先清理旧请求再发起新请求,确保新数据覆盖旧数据。如果直接新请求覆盖旧请求,可能出现"旧请求后完成 → 数据回退"的 bug。
Q 多个 useEffect 的执行顺序是怎样的?
A 按代码书写顺序依次执行。所以应该把"初始化类"的 useEffect 放在前面(如数据加载),"响应变化"的 useEffect 放在后面(如同步状态到 localStorage)。同一次渲染中,所有 useEffect 按顺序执行。
Q useEffect 和 useLayoutEffect 有什么区别?
A 执行时机不同:useEffect 在 DOM 更新后异步执行(浏览器绘制之后),用户不会看到闪烁;useLayoutEffect 在 DOM 更新后同步执行(浏览器绘制之前),可以修改 DOM 再让浏览器绘制。99% 的场景用 useEffect。useLayoutEffect 只在"需要根据 DOM 布局计算再同步修改 DOM"时使用(如测量元素尺寸后调整位置)。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 PageTitle 组件,使用 useEffect 在组件挂载时把页面标题改为"欢迎访问我的网站",卸载时恢复为原标题。
  2. 进阶题(难度⭐⭐):创建一个 OnlineStatus 组件,使用 useEffect 监听 navigator.onLine 和 online/offline 事件,实时显示网络状态。
  3. 挑战题(难度⭐⭐⭐):创建一个 usePolling 自定义 Hook,接收 url 和 interval 参数,每 interval 毫秒轮询一次接口,返回 { data, isLoading, error },支持暂停/恢复轮询。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏