Skip to content

React 生命周期

概述

React 组件的生命周期是指组件从创建到销毁的完整过程。理解生命周期对于优化组件性能、处理副作用和管理资源至关重要。本章将学习函数组件的 useEffect Hook 和类组件的生命周期方法。

🔄 函数组件生命周期(Hooks)

useEffect 基础

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

function BasicLifecycle() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // 组件挂载和更新时都会执行
  useEffect(() => {
    console.log('组件渲染了');
    document.title = `计数: ${count}`;
  });
  
  // 只在组件挂载时执行一次
  useEffect(() => {
    console.log('组件挂载了');
    // 模拟从 API 获取数据
    fetch('/api/user')
      .then(response => response.json())
      .then(data => setName(data.name))
      .catch(error => console.error('获取数据失败:', error));
  }, []); // 空依赖数组
  
  // 只在 count 变化时执行
  useEffect(() => {
    console.log('count 变化了:', count);
    if (count > 5) {
      alert('计数超过 5 了!');
    }
  }, [count]); // count 依赖
  
  return (
    <div>
      <h2>生命周期演示</h2>
      <p>姓名: {name}</p>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加计数
      </button>
    </div>
  );
}

清理副作用

jsx
function CleanupExample() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  useEffect(() => {
    let interval = null;
    
    if (isRunning) {
      console.log('启动定时器');
      interval = setInterval(() => {
        setSeconds(seconds => seconds + 1);
      }, 1000);
    }
    
    // 清理函数
    return () => {
      if (interval) {
        console.log('清理定时器');
        clearInterval(interval);
      }
    };
  }, [isRunning]);
  
  // 组件卸载时的清理
  useEffect(() => {
    const handleBeforeUnload = () => {
      console.log('页面即将卸载');
    };
    
    window.addEventListener('beforeunload', handleBeforeUnload);
    
    return () => {
      console.log('移除事件监听器');
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, []);
  
  const reset = () => {
    setSeconds(0);
    setIsRunning(false);
  };
  
  return (
    <div>
      <h2>定时器: {seconds}s</h2>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? '暂停' : '开始'}
      </button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

数据获取和状态管理

jsx
function DataFetching() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  
  useEffect(() => {
    let isCancelled = false;
    
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // 模拟 API 调用
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        const response = await fetch(`/api/users?page=${page}`);
        const data = await response.json();
        
        // 检查组件是否仍然挂载
        if (!isCancelled) {
          setUsers(data.users);
          setLoading(false);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message);
          setLoading(false);
        }
      }
    };
    
    fetchUsers();
    
    // 清理函数,防止内存泄漏
    return () => {
      isCancelled = true;
    };
  }, [page]); // 依赖 page,当 page 变化时重新获取数据
  
  if (loading) {
    return <div>加载中...</div>;
  }
  
  if (error) {
    return <div>错误: {error}</div>;
  }
  
  return (
    <div>
      <h2>用户列表</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button 
        onClick={() => setPage(page - 1)} 
        disabled={page <= 1}
      >
        上一页
      </button>
      <span> 第 {page} 页 </span>
      <button onClick={() => setPage(page + 1)}>
        下一页
      </button>
    </div>
  );
}

🏗️ 类组件生命周期

挂载阶段

jsx
class MountingLifecycle extends React.Component {
  constructor(props) {
    super(props);
    console.log('1. constructor: 组件构造');
    this.state = {
      data: null,
      loading: true
    };
  }
  
  static getDerivedStateFromProps(nextProps, prevState) {
    console.log('2. getDerivedStateFromProps: 从 props 派生状态');
    // 返回新状态或 null
    return null;
  }
  
  componentDidMount() {
    console.log('4. componentDidMount: 组件挂载完成');
    // 在这里进行 API 调用、订阅事件等
    this.fetchData();
  }
  
  render() {
    console.log('3. render: 渲染组件');
    const { data, loading } = this.state;
    
    if (loading) {
      return <div>加载中...</div>;
    }
    
    return (
      <div>
        <h2>类组件挂载演示</h2>
        <p>数据: {data}</p>
        <button onClick={this.handleRefresh}>刷新数据</button>
      </div>
    );
  }
  
  fetchData = async () => {
    try {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.setState({ 
        data: '从服务器获取的数据', 
        loading: false 
      });
    } catch (error) {
      console.error('数据获取失败:', error);
      this.setState({ loading: false });
    }
  };
  
  handleRefresh = () => {
    this.setState({ loading: true });
    this.fetchData();
  };
}

更新阶段

jsx
class UpdatingLifecycle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: 'React'
    };
  }
  
  static getDerivedStateFromProps(nextProps, prevState) {
    console.log('getDerivedStateFromProps: props 变化');
    return null;
  }
  
  shouldComponentUpdate(nextProps, nextState) {
    console.log('shouldComponentUpdate: 是否应该更新');
    // 返回 false 可以阻止组件更新
    // 这里我们阻止 count 为偶数时的更新(仅作演示)
    return nextState.count % 2 !== 0 || nextState.name !== this.state.name;
  }
  
  render() {
    console.log('render: 重新渲染');
    const { count, name } = this.state;
    
    return (
      <div>
        <h2>类组件更新演示</h2>
        <p>计数: {count}</p>
        <p>名称: {name}</p>
        <button onClick={this.incrementCount}>增加计数</button>
        <button onClick={this.changeName}>改变名称</button>
        <p><small>注意:偶数计数不会触发视图更新</small></p>
      </div>
    );
  }
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('getSnapshotBeforeUpdate: 更新前快照');
    // 返回值会传递给 componentDidUpdate
    if (prevState.count !== this.state.count) {
      return { prevCount: prevState.count };
    }
    return null;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('componentDidUpdate: 组件更新完成');
    if (snapshot && snapshot.prevCount !== undefined) {
      console.log(`计数从 ${snapshot.prevCount} 更新到 ${this.state.count}`);
    }
  }
  
  incrementCount = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };
  
  changeName = () => {
    this.setState({ 
      name: this.state.name === 'React' ? 'Vue' : 'React' 
    });
  };
}

卸载阶段

jsx
class UnmountingExample extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
    this.interval = null;
  }
  
  componentDidMount() {
    console.log('组件挂载,启动定时器');
    this.interval = setInterval(() => {
      this.setState(prevState => ({ seconds: prevState.seconds + 1 }));
    }, 1000);
    
    // 添加事件监听器
    window.addEventListener('resize', this.handleResize);
  }
  
  componentWillUnmount() {
    console.log('组件即将卸载,清理资源');
    
    // 清理定时器
    if (this.interval) {
      clearInterval(this.interval);
    }
    
    // 移除事件监听器
    window.removeEventListener('resize', this.handleResize);
    
    // 取消网络请求
    // this.abortController.abort();
  }
  
  handleResize = () => {
    console.log('窗口大小改变');
  };
  
  render() {
    return (
      <div>
        <h2>运行时间: {this.state.seconds}s</h2>
        <p>调整窗口大小查看事件监听器</p>
      </div>
    );
  }
}

// 容器组件,用于演示卸载
function UnmountingContainer() {
  const [showComponent, setShowComponent] = useState(true);
  
  return (
    <div>
      <button onClick={() => setShowComponent(!showComponent)}>
        {showComponent ? '卸载组件' : '挂载组件'}
      </button>
      {showComponent && <UnmountingExample />}
    </div>
  );
}

🔧 自定义 Hooks

封装生命周期逻辑

jsx
// 自定义 Hook:组件挂载状态
function useIsMounted() {
  const isMounted = useRef(false);
  
  useEffect(() => {
    isMounted.current = true;
    return () => {
      isMounted.current = false;
    };
  }, []);
  
  return isMounted;
}

// 自定义 Hook:防抖
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

// 自定义 Hook:数据获取
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isMounted = useIsMounted();
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url);
        const result = await response.json();
        
        if (isMounted.current) {
          setData(result);
        }
      } catch (err) {
        if (isMounted.current) {
          setError(err.message);
        }
      } finally {
        if (isMounted.current) {
          setLoading(false);
        }
      }
    };
    
    if (url) {
      fetchData();
    }
  }, [url, isMounted]);
  
  return { data, loading, error };
}

// 使用自定义 Hooks
function CustomHooksExample() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  const { data, loading, error } = useApi(
    debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
  );
  
  return (
    <div>
      <h2>自定义 Hooks 演示</h2>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="输入搜索内容..."
        style={{ padding: '8px', width: '200px' }}
      />
      
      {loading && <p>搜索中...</p>}
      {error && <p>错误: {error}</p>}
      {data && (
        <div>
          <h3>搜索结果:</h3>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

🎯 生命周期最佳实践

性能优化

jsx
function PerformanceOptimization() {
  const [posts, setPosts] = useState([]);
  const [currentUser, setCurrentUser] = useState(null);
  const [theme, setTheme] = useState('light');
  
  // 避免在每次渲染时创建新的对象
  const memoizedUser = useMemo(() => {
    return currentUser ? {
      ...currentUser,
      fullName: `${currentUser.firstName} ${currentUser.lastName}`
    } : null;
  }, [currentUser]);
  
  // 缓存昂贵的计算
  const expensiveValue = useMemo(() => {
    console.log('执行昂贵的计算');
    return posts.reduce((total, post) => total + post.likes, 0);
  }, [posts]);
  
  // 缓存回调函数
  const handlePostLike = useCallback((postId) => {
    setPosts(posts => posts.map(post =>
      post.id === postId 
        ? { ...post, likes: post.likes + 1 }
        : post
    ));
  }, []);
  
  // 获取数据
  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('/api/posts');
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('获取帖子失败:', error);
      }
    };
    
    fetchPosts();
  }, []);
  
  return (
    <div className={`app-${theme}`}>
      <h2>性能优化演示</h2>
      
      {memoizedUser && (
        <div>欢迎, {memoizedUser.fullName}!</div>
      )}
      
      <div>总点赞数: {expensiveValue}</div>
      
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
      
      <PostList posts={posts} onLike={handlePostLike} />
    </div>
  );
}

// 被优化的子组件
const PostList = React.memo(function PostList({ posts, onLike }) {
  console.log('PostList 渲染');
  
  return (
    <div>
      {posts.map(post => (
        <PostItem key={post.id} post={post} onLike={onLike} />
      ))}
    </div>
  );
});

const PostItem = React.memo(function PostItem({ post, onLike }) {
  console.log('PostItem 渲染:', post.title);
  
  return (
    <div style={{ border: '1px solid #ddd', padding: '10px', margin: '10px 0' }}>
      <h3>{post.title}</h3>
      <p>点赞: {post.likes}</p>
      <button onClick={() => onLike(post.id)}>
        👍 点赞
      </button>
    </div>
  );
});

错误边界

jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    // 更新状态以显示错误 UI
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // 记录错误到错误报告服务
    console.error('错误边界捕获到错误:', error, errorInfo);
    
    // 可以将错误发送到错误监控服务
    // logErrorToService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '20px', border: '1px solid red', margin: '10px' }}>
          <h2>出现了错误</h2>
          <p>应用遇到了一个错误,请刷新页面重试。</p>
          <details>
            <summary>错误详情</summary>
            <pre>{this.state.error && this.state.error.toString()}</pre>
          </details>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 可能出错的组件
function BuggyComponent() {
  const [count, setCount] = useState(0);
  
  if (count > 3) {
    throw new Error('计数器爆炸了!');
  }
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加计数(超过 3 会出错)
      </button>
    </div>
  );
}

// 使用错误边界
function ErrorBoundaryExample() {
  return (
    <div>
      <h2>错误边界演示</h2>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

📝 本章小结

通过本章学习,你应该掌握了:

函数组件生命周期

  • ✅ useEffect Hook 的使用方法
  • ✅ 依赖数组的作用和最佳实践
  • ✅ 清理副作用防止内存泄漏
  • ✅ 自定义 Hooks 封装生命周期逻辑

类组件生命周期

  • ✅ 挂载、更新、卸载三个阶段
  • ✅ 各个生命周期方法的用途
  • ✅ 性能优化和错误处理
  • ✅ 从类组件到函数组件的迁移

最佳实践

  1. 资源清理:及时清理定时器、事件监听器
  2. 依赖优化:正确设置 useEffect 依赖
  3. 性能监控:使用 React DevTools Profiler
  4. 错误处理:实现错误边界组件
  5. 代码组织:使用自定义 Hooks 复用逻辑

继续学习下一章 - React 组件间通信

本站内容仅供学习和研究使用。