React: useEffect 与生命周期

最后更新:2026-08-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. fetchsetUser(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) 定时器的清理

JSX
function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)

    // ✅ 组件卸载时清除定时器
    return () => {
      clearInterval(timer)
      console.log('定时器已清除')
    }
  }, [])  // 只在挂载时启动

  return <p>已运行:{seconds} 秒</p>
}
// 不清理的话:组件卸载后定时器还在跑,报错"在已卸载组件上更新状态"
▶ 试一试

(2) 事件监听的清理

JSX
function WindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight })
    }

    window.addEventListener('resize', handleResize)

    // ✅ 组件卸载时移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <p>窗口:{size.width} × {size.height}</p>
}
▶ 试一试

(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 → 不更新状态(避免闪现) 
▶ 试一试

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

JSX 📖 仅展示
// ============================================
// 示例:useEffect 声明周期演示
// 功能:挂载时启动计时器 + 监听键盘事件
//       卸载时清理所有资源
// ============================================

function LifecycleDemo() {
  const [count, setCount] = useState(0)
  const [logs, setLogs] = useState([])

  // 效果 1:标题更新
  useEffect(() => {
    document.title = `计数:${count}`
  }, [count])

  // 效果 2:计时器
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1)
    }, 2000)

    addLog('⏱️ 计时器已启动')

    return () => {
      clearInterval(id)
      addLog('⏱️ 计时器已清理')
    }
  }, [])

  // 效果 3:键盘事件
  useEffect(() => {
    function handleKey(e) {
      if (e.key === ' ') {
        setCount(c => c + 5)
      }
    }
    window.addEventListener('keydown', handleKey)

    return () => {
      window.removeEventListener('keydown', handleKey)
      addLog('⌨️ 键盘监听已移除')
    }
  }, [])

  function addLog(msg) {
    setLogs(prev => [...prev, `${new Date().toLocaleTimeString()} ${msg}`])
  }

  return (
    <div>
      <h2>useEffect 生命周期演示</h2>
      <p>计数:{count}(每 2 秒 +1,按空格键 +5)</p>
      <div style={{ maxHeight: '200px', overflow: 'auto', backgroundColor: '#f5f5f5', padding: '10px', borderRadius: '4px', fontSize: '12px', fontFamily: 'monospace' }}>
        {logs.map((log, i) => <div key={i}>{log}</div>)}
      </div>
    </div>
  )
}
逻辑代码 41 行(超过 40 行限制,仅展示)

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
类组件生命周期 对应的 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
// 组件卸载时 → 断开当前连接(清理函数执行)
▶ 试一试

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 尺寸、读取布局、同步动画
JSX
import { useState, useEffect, useLayoutEffect } from 'react'

function MeasureExample() {
  const [width, setWidth] = useState(0)
  const ref = useRef(null)

  // ❌ useEffect 读取 DOM 尺寸 → 用户可能看到闪烁
  useEffect(() => {
    setWidth(ref.current.offsetWidth)  // 绘制后读取 → 可能触发额外渲染
  }, [])

  // ✅ useLayoutEffect → 绘制前读取,用户无感知
  useLayoutEffect(() => {
    setWidth(ref.current.offsetWidth)
  }, [])

  return <div ref={ref}>宽度:{width}px</div>
}
▶ 试一试

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



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

JSX
// ============================================
// 完整示例:数据仪表盘(useEffect 综合应用)
// 功能:定时刷新数据 + 窗口自适应 + 资源清理
// ============================================

import { useState, useEffect } from 'react'

function Dashboard() {
  const [data, setData] = useState(null)
  const [isLive, setIsLive] = useState(true)
  const [lastUpdate, setLastUpdate] = useState(null)
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })

  // 效果 1:首次加载 + 定时刷新
  useEffect(() => {
    function fetchData() {
      // 模拟 API 请求
      const mockData = {
        users: Math.floor(Math.random() * 1000) + 500,
        orders: Math.floor(Math.random() * 200) + 100,
        revenue: Math.floor(Math.random() * 50000) + 10000,
        growth: (Math.random() * 20 - 5).toFixed(1)
      }
      setData(mockData)
      setLastUpdate(new Date().toLocaleTimeString())
    }

    // 立即执行一次
    fetchData()

    // 定时刷新
    let interval
    if (isLive) {
      interval = setInterval(fetchData, 5000)
    }

    return () => {
      clearInterval(interval)
    }
  }, [isLive])  // isLive 变化时重新设置定时器

  // 效果 2:窗口大小监听
  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  // 效果 3:页面标题
  useEffect(() => {
    document.title = `仪表盘 - ${data ? `${data.users} 用户` : '加载中...'}`
  }, [data])

  // 效果 4:离开页面提示
  useEffect(() => {
    function handleBeforeUnload(e) {
      if (isLive) {
        e.preventDefault()
        e.returnValue = ''
      }
    }
    window.addEventListener('beforeunload', handleBeforeUnload)
    return () => window.removeEventListener('beforeunload', handleBeforeUnload)
  }, [isLive])

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
        <h2>📊 实时仪表盘</h2>
        <div>
          <span style={{ color: '#666', fontSize: '14px', marginRight: '12px' }}>
            窗口:{windowSize.width}×{windowSize.height}
          </span>
          <label>
            <input type="checkbox" checked={isLive} onChange={e => setIsLive(e.target.checked)} />
            实时刷新
          </label>
        </div>
      </div>

      {!data ? (
        <p style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
          ⏳ 加载中...
        </p>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
          <StatCard label="总用户" value={data.users.toLocaleString()} color="#1890ff" />
          <StatCard label="今日订单" value={data.orders.toLocaleString()} color="#52c41a" />
          <StatCard label="月收入" value={`$${data.revenue.toLocaleString()}`} color="#ff4d4f" />
          <StatCard label="增长率" value={`${data.growth}%`} color={parseFloat(data.growth) >= 0 ? '#52c41a' : '#ff4d4f'} />
        </div>
      )}

      <p style={{ textAlign: 'right', color: '#999', fontSize: '12px', marginTop: '12px' }}>
        最后更新:{lastUpdate} {isLive ? '(每 5 秒自动刷新)' : '(已暂停)'}
      </p>
    </div>
  )
}

function StatCard({ label, value, color }) {
  return (
    <div style={{
      border: '1px solid #f0f0f0',
      borderRadius: '8px',
      padding: '20px',
      textAlign: 'center',
      boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
    }}>
      <p style={{ color: '#666', margin: '0 0 8px 0' }}>{label}</p>
      <p style={{ fontSize: '28px', fontWeight: 'bold', color, margin: 0 }}>{value}</p>
    </div>
  )
}

预期输出:数据仪表盘,含 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>
  )
}
▶ 试一试

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

JSX
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight })
    }
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return size
}

function ResponsiveLayout() {
  const { width } = useWindowSize()
  const layout = width < 640 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop'

  return (
    <div style={{ padding: 16, maxWidth: 600, margin: '0 auto' }}>
      <h3>Viewport: {width}px ({layout})</h3>
      {layout === 'mobile' && <p>📱 Single column layout</p>}
      {layout === 'tablet' && <p>📟 Two column layout</p>}
      {layout === 'desktop' && <p>💻 Three column layout with sidebar</p>}
    </div>
  )
}
▶ 试一试

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

JSX 📖 仅展示
function usePersistedState(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key)
      return saved !== null ? JSON.parse(saved) : initial
    } catch { return initial }
  })

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)) }
    catch (e) { console.error('Save failed:', e) }
  }, [key, value])

  return [value, setValue]
}

function TodoApp() {
  const [todos, setTodos] = usePersistedState('todos', [])
  const [input, setInput] = useState('')

  function addTodo() {
    if (!input.trim()) return
    setTodos(prev => [...prev, { id: Date.now(), text: input.trim(), done: false }])
    setInput('')
  }

  function toggleTodo(id) {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
  }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Todos (persisted)</h3>
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <input value={input} onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="Add task..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
        <button onClick={addTodo} style={{ padding: '8px 16px', cursor: 'pointer' }}>Add</button>
      </div>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(t => (
          <li key={t.id} onClick={() => toggleTodo(t.id)}
            style={{ padding: 4, cursor: 'pointer', textDecoration: t.done ? 'line-through' : 'none', color: t.done ? '#999' : '#333' }}>
            {t.text}
          </li>
        ))}
      </ul>
    </div>
  )
}
逻辑代码 44 行(超过 40 行限制,仅展示)

❓ 常见问题

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.onLineonline/offline 事件,实时显示网络状态。
  3. 挑战题(难度⭐⭐⭐):创建一个 usePolling 自定义 Hook,接收 urlinterval 参数,每 interval 毫秒轮询一次接口,返回 { data, isLoading, error },支持暂停/恢复轮询。
Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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