React生命周期与useEffect

生命周期是理解组件行为的关键,而useEffect Hook是处理副作用(数据获取、订阅、手动DOM操作等)的核心工具。

什么是生命周期?

生命周期是指组件从创建到销毁的完整过程。React组件在不同阶段会调用特定的方法,这些方法被称为生命周期方法。

React组件生命周期

挂载阶段 (Mounting)

组件被创建并插入到DOM中的过程

更新阶段 (Updating)

组件重新渲染的过程,通常由props或state的变化引起

卸载阶段 (Unmounting)

组件从DOM中移除的过程

类组件的生命周期方法

在Hooks出现之前,生命周期方法只能在类组件中使用。了解这些方法有助于理解useEffect的工作方式。

1
constructor()
初始化state、绑定事件
2
render()
渲染组件UI
3
componentDidMount()
组件挂载后执行
4
componentDidUpdate()
组件更新后执行
5
componentWillUnmount()
组件卸载前执行

类组件生命周期示例


import React from 'react';

class LifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      data: null
    };
    console.log('constructor: 组件初始化');
  }

  componentDidMount() {
    console.log('componentDidMount: 组件已挂载');
    // 通常在这里进行数据获取
    this.fetchData();
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate: 组件已更新');

    // 检查count是否变化
    if (prevState.count !== this.state.count) {
      console.log(`count从 ${prevState.count} 变为 ${this.state.count}`);
    }
  }

  componentWillUnmount() {
    console.log('componentWillUnmount: 组件即将卸载');
    // 清理工作,如清除定时器、取消订阅等
    clearInterval(this.timerId);
  }

  fetchData = async () => {
    // 模拟API调用
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    this.setState({ data });
  };

  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  render() {
    console.log('render: 渲染组件');
    return (
      <div>
        <h3>计数器: {this.state.count}</h3>
        <button onClick={this.increment}>增加</button>
        {this.state.data && (
          <p>数据: {JSON.stringify(this.state.data)}</p>
        )}
      </div>
    );
  }
}
                    

useEffect Hook介绍

useEffect是React提供的Hook,用于在函数组件中执行副作用操作。它可以看作是componentDidMountcomponentDidUpdatecomponentWillUnmount的组合。

useEffect基本语法


import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // useEffect的基本用法
  useEffect(() => {
    // 这里的代码在每次渲染后执行
    console.log(`计数已更新: ${count}`);

    // 清理函数(可选)
    return () => {
      console.log('清理上一次的effect');
    };
  });

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        点击增加
      </button>
    </div>
  );
}
                        

useEffect的依赖数组

useEffect的第二个参数是一个依赖数组,用于控制effect的执行时机:

空数组 []

effect只在组件挂载和卸载时执行


useEffect(() => {
  console.log('只执行一次');
}, []);
                                    
有依赖的数组

当依赖项变化时执行


useEffect(() => {
  console.log('count变化时执行');
}, [count]);
                                    
没有依赖数组

每次渲染后都执行


useEffect(() => {
  console.log('每次渲染都执行');
});
                                    

useEffect与生命周期的对应关系

类组件生命周期方法 useEffect等效写法 说明
componentDidMount()

useEffect(() => {
  // 挂载时的逻辑
}, []);
                                        
空依赖数组确保effect只在挂载时运行一次
componentDidUpdate(prevProps, prevState)

useEffect(() => {
  // 更新时的逻辑
});

// 或监听特定依赖
useEffect(() => {
  // 当count变化时执行
}, [count]);
                                        
无依赖数组每次渲染都执行,有依赖数组在依赖变化时执行
componentWillUnmount()

useEffect(() => {
  return () => {
    // 清理逻辑
  };
}, []);
                                        
返回一个清理函数,在组件卸载时执行
shouldComponentUpdate()

const MyComponent = React.memo((props) => {
  // 组件逻辑
});
                                        
使用React.memo进行性能优化

useEffect常见用例

1. 数据获取


import React, { useState, useEffect } from 'react';

function DataFetching() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 定义异步函数
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://api.example.com/posts');
        if (!response.ok) {
          throw new Error('网络响应异常');
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 清理函数:取消请求(如果支持AbortController)
    return () => {
      // 这里可以添加取消请求的逻辑
    };
  }, []); // 空数组:只执行一次

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      <h3>数据列表</h3>
      <ul>
        {data && data.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}
                    
数据获取注意事项

在useEffect中直接使用async函数会导致问题,因为useEffect的清理函数需要同步执行。应该将async函数定义在effect内部或外部,然后在effect中调用它。

2. 事件监听


import React, { useState, useEffect } from 'react';

function WindowResizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    // 处理窗口大小变化的函数
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // 添加事件监听
    window.addEventListener('resize', handleResize);

    // 立即调用一次以获取初始尺寸
    handleResize();

    // 清理函数:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空数组:只在挂载和卸载时执行

  return (
    <div>
      <h3>窗口尺寸监控</h3>
      <p>宽度: {windowSize.width}px</p>
      <p>高度: {windowSize.height}px</p>
      <p>设备类型: {windowSize.width < 768 ? '手机' : windowSize.width < 1024 ? '平板' : '桌面'}</p>
    </div>
  );
}
                    

3. 定时器


import React, { useState, useEffect } from 'react';

function Timer() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    let timerId = null;

    if (isRunning) {
      timerId = setInterval(() => {
        setTime(prevTime => prevTime + 1);
      }, 1000);
    }

    // 清理函数:清除定时器
    return () => {
      if (timerId) {
        clearInterval(timerId);
      }
    };
  }, [isRunning]); // 依赖isRunning:当isRunning变化时重新执行

  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  };

  return (
    <div className="timer">
      <h3>定时器: {formatTime(time)}</h3>
      <div className="timer-controls">
        <button onClick={() => setIsRunning(!isRunning)}>
          {isRunning ? '暂停' : '开始'}
        </button>
        <button onClick={() => setTime(0)} disabled={isRunning}>
          重置
        </button>
      </div>
    </div>
  );
}
                    

4. 订阅外部数据源


import React, { useState, useEffect } from 'react';

// 模拟外部数据源
class MockWebSocket {
  constructor(url) {
    this.url = url;
    this.listeners = [];
  }

  connect() {
    console.log(`连接到 ${this.url}`);
    this.connected = true;
  }

  disconnect() {
    console.log(`断开连接 ${this.url}`);
    this.connected = false;
  }

  subscribe(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter(listener => listener !== callback);
    };
  }

  // 模拟发送消息
  simulateMessage(message) {
    this.listeners.forEach(listener => listener(message));
  }
}

function WebSocketComponent() {
  const [messages, setMessages] = useState([]);
  const [connectionStatus, setConnectionStatus] = useState('未连接');

  useEffect(() => {
    // 创建WebSocket连接
    const ws = new MockWebSocket('ws://api.example.com/ws');
    ws.connect();
    setConnectionStatus('已连接');

    // 订阅消息
    const unsubscribe = ws.subscribe((message) => {
      setMessages(prev => [...prev, message]);
    });

    // 模拟收到消息
    const intervalId = setInterval(() => {
      if (ws.connected) {
        ws.simulateMessage({
          id: Date.now(),
          content: `消息 ${messages.length + 1}`,
          timestamp: new Date().toISOString()
        });
      }
    }, 3000);

    // 清理函数:断开连接和取消订阅
    return () => {
      console.log('清理WebSocket连接');
      clearInterval(intervalId);
      ws.disconnect();
      setConnectionStatus('已断开');
      unsubscribe();
    };
  }, []); // 空数组:只执行一次

  return (
    <div>
      <h3>WebSocket消息</h3>
      <p>连接状态: {connectionStatus}</p>
      <ul>
        {messages.map(msg => (
          <li key={msg.id}>
            {msg.content} ({new Date(msg.timestamp).toLocaleTimeString()})
          </li>
        ))}
      </ul>
    </div>
  );
}
                    

useEffect高级用法

1. 多个useEffect

可以将不同的副作用逻辑分离到多个useEffect中,使代码更清晰:


function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [onlineStatus, setOnlineStatus] = useState('离线');

  // 获取用户信息
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]); // 当userId变化时重新获取

  // 获取用户帖子
  useEffect(() => {
    fetch(`/api/users/${userId}/posts`)
      .then(res => res.json())
      .then(data => setPosts(data));
  }, [userId]);

  // 监听在线状态
  useEffect(() => {
    const handleOnline = () => setOnlineStatus('在线');
    const handleOffline = () => setOnlineStatus('离线');

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // 空数组:只执行一次

  // 更多useEffect...
}
                    

2. 跳过不必要的effect执行


function ExpensiveComponent({ data, filter }) {
  const [filteredData, setFilteredData] = useState([]);

  useEffect(() => {
    // 只有当data或filter变化时才执行昂贵的过滤操作
    console.log('执行昂贵的过滤操作...');

    const result = data.filter(item => {
      // 模拟复杂过滤逻辑
      return item.name.includes(filter);
    });

    setFilteredData(result);
  }, [data, filter]); // 依赖数组:只在data或filter变化时执行

  return (
    <div>
      <p>过滤条件: {filter}</p>
      <p>结果数量: {filteredData.length}</p>
    </div>
  );
}
                    

3. 在effect中访问最新的state和props


function CounterWithLog() {
  const [count, setCount] = useState(0);
  const [log, setLog] = useState([]);

  useEffect(() => {
    // 使用函数式更新确保获取最新的state
    setLog(prevLog => [
      ...prevLog,
      `计数更新为: ${count} (时间: ${new Date().toLocaleTimeString()})`
    ]);
  }, [count]); // 依赖count:当count变化时执行

  return (
    <div>
      <h3>计数器: {count}</h3>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <div>
        <h4>更新日志:</h4>
        <ul>
          {log.map((entry, index) => (
            <li key={index}>{entry}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}
                    

常见错误与解决方案

当effect中更新了依赖的状态,但没有正确设置依赖数组时,会导致无限循环:


// ❌ 错误:导致无限循环
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(count + 1); // 更新count,触发重新渲染
}); // 没有依赖数组,每次渲染后都执行

// ✅ 正确:使用空数组或确保逻辑正确
useEffect(() => {
  // 只执行一次的初始化逻辑
}, []); // 空数组:只执行一次
                                    

如果effect中使用了某个状态或prop,但依赖数组中遗漏了它,可能导致bug:


const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);

// ❌ 错误:遗漏了multiplier依赖
useEffect(() => {
  console.log(`结果: ${count * multiplier}`);
}, [count]); // 遗漏了multiplier

// ✅ 正确:包含所有依赖
useEffect(() => {
  console.log(`结果: ${count * multiplier}`);
}, [count, multiplier]); // 包含所有依赖
                                    

对于需要清理的副作用(如事件监听、定时器、订阅),必须提供清理函数:


// ❌ 错误:可能导致内存泄漏
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// ✅ 正确:提供清理函数
useEffect(() => {
  window.addEventListener('resize', handleResize);

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

useEffect最佳实践

推荐做法
  • 将不相关的副作用拆分到多个useEffect中
  • 为所有需要清理的副作用提供清理函数
  • 使用依赖数组精确控制effect执行时机
  • 将effect内部的函数定义在effect中或使用useCallback
  • 使用自定义Hook封装复杂的副作用逻辑
  • 处理异步操作时注意竞态条件
避免做法
  • 避免在effect中执行渲染期间的工作
  • 不要忘记依赖数组中的依赖项
  • 避免在effect中直接修改DOM(除非必要)
  • 不要在循环、条件或嵌套函数中调用useEffect
  • 避免在effect中进行昂贵的计算
  • 不要依赖不稳定的依赖项

完整示例:数据仪表盘


import React, { useState, useEffect, useCallback } from 'react';

function DataDashboard() {
  const [dashboardData, setDashboardData] = useState({
    users: 0,
    posts: 0,
    comments: 0,
    lastUpdated: null
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [autoRefresh, setAutoRefresh] = useState(false);

  // 获取数据函数
  const fetchDashboardData = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      // 模拟并行获取多个数据源
      const [usersRes, postsRes, commentsRes] = await Promise.all([
        fetch('/api/users/count'),
        fetch('/api/posts/count'),
        fetch('/api/comments/count')
      ]);

      if (!usersRes.ok || !postsRes.ok || !commentsRes.ok) {
        throw new Error('获取数据失败');
      }

      const usersCount = await usersRes.json();
      const postsCount = await postsRes.json();
      const commentsCount = await commentsRes.json();

      setDashboardData({
        users: usersCount,
        posts: postsCount,
        comments: commentsCount,
        lastUpdated: new Date().toISOString()
      });
    } catch (err) {
      setError(err.message);
      console.error('获取数据失败:', err);
    } finally {
      setLoading(false);
    }
  }, []);

  // 初始加载和手动刷新
  useEffect(() => {
    fetchDashboardData();
  }, [fetchDashboardData]);

  // 自动刷新定时器
  useEffect(() => {
    let intervalId = null;

    if (autoRefresh) {
      intervalId = setInterval(() => {
        console.log('自动刷新数据...');
        fetchDashboardData();
      }, 30000); // 每30秒刷新一次
    }

    return () => {
      if (intervalId) {
        clearInterval(intervalId);
      }
    };
  }, [autoRefresh, fetchDashboardData]);

  // 窗口焦点时刷新
  useEffect(() => {
    const handleFocus = () => {
      if (document.visibilityState === 'visible') {
        console.log('窗口重新获得焦点,刷新数据');
        fetchDashboardData();
      }
    };

    document.addEventListener('visibilitychange', handleFocus);

    return () => {
      document.removeEventListener('visibilitychange', handleFocus);
    };
  }, [fetchDashboardData]);

  // 格式化时间
  const formatTime = (isoString) => {
    if (!isoString) return '未更新';
    return new Date(isoString).toLocaleString();
  };

  return (
    <div className="data-dashboard">
      <h2>数据仪表盘</h2>

      <div className="dashboard-controls">
        <button
          onClick={fetchDashboardData}
          disabled={loading}
          className="btn btn-primary"
        >
          {loading ? '加载中...' : '刷新数据'}
        </button>
        <label>
          <input
            type="checkbox"
            checked={autoRefresh}
            onChange={(e) => setAutoRefresh(e.target.checked)}
          />
          自动刷新 (30秒)
        </label>
      </div>

      {error && (
        <div className="alert alert-danger">
          错误: {error}
        </div>
      )}

      <div className="dashboard-stats">
        <div className="stat-card">
          <h3>用户数</h3>
          <p className="stat-value">{dashboardData.users.toLocaleString()}</p>
        </div>
        <div className="stat-card">
          <h3>帖子数</h3>
          <p className="stat-value">{dashboardData.posts.toLocaleString()}</p>
        </div>
        <div className="stat-card">
          <h3>评论数</h3>
          <p className="stat-value">{dashboardData.comments.toLocaleString()}</p>
        </div>
      </div>

      <div className="dashboard-footer">
        <p>最后更新: {formatTime(dashboardData.lastUpdated)}</p>
        {loading && <p className="text-muted">数据加载中...</p>}
      </div>
    </div>
  );
}
                    

本章要点总结

  • useEffect用于处理组件中的副作用(数据获取、订阅、事件监听等)
  • 依赖数组控制effect的执行时机:空数组只执行一次,有依赖则在依赖变化时执行
  • 清理函数用于清除副作用(取消订阅、移除事件监听、清除定时器等)
  • 可以将不相关的副作用拆分到多个useEffect中
  • 注意避免无限循环、遗漏依赖项和忘记清理函数等常见错误
  • useEffect可以模拟类组件的生命周期方法