← 返回目录 | 下一课:Hooks详解 →

第7课: 生命周期

组件生命周期详解

🎯 学习目标

1. 生命周期概述

生命周期定义:组件生命周期是指组件从创建到销毁的整个过程。React提供了多个钩子方法,让我们可以在组件的不同阶段执行特定的操作。

生命周期的三个阶段

类组件生命周期流程

constructor
componentDidMount
render
componentDidUpdate
componentWillUnmount

2. 类组件的生命周期方法

2.1 挂载阶段

class LifecycleExample extends React.Component {
    constructor(props) {
        super(props);
        console.log('1. constructor - 组件构造函数');
        this.state = { count: 0 };
    }
    
    componentDidMount() {
        console.log('3. componentDidMount - 组件已挂载');
        
        // 在这里执行副作用操作
        document.title = '组件已挂载';
        
        // 发起网络请求
        this.fetchData();
    }
    
    fetchData() {
        fetch('/api/data')
            .then(response => response.json())
            .then(data => {
                this.setState({ data });
            });
    }
    
    render() {
        console.log('2. render - 渲染组件');
        return <div>Count: {this.state.count}</div>;
    }
}

2.2 更新阶段

class UpdatingExample extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }
    
    shouldComponentUpdate(nextProps, nextState) {
        console.log('shouldComponentUpdate - 是否应该更新');
        
        // 返回false可以阻止组件更新
        return nextState.count !== this.state.count;
    }
    
    getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log('getSnapshotBeforeUpdate - 更新前获取快照');
        
        // 返回一个值,将传递给componentDidUpdate
        return { scrollPosition: window.scrollY };
    }
    
    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('componentDidUpdate - 组件已更新');
        
        if (snapshot) {
            console.log('更新前的滚动位置:', snapshot.scrollPosition);
        }
        
        // 在这里执行更新后的副作用操作
        if (this.props.user !== prevProps.user) {
            this.fetchUserData(this.props.user);
        }
    }
    
    render() {
        return <div>Count: {this.state.count}</div>;
    }
}

2.3 卸载阶段

class UnmountingExample extends React.Component {
    state = { timer: null };
    
    componentDidMount() {
        // 设置定时器
        const timer = setInterval(() => {
            console.log('定时器执行');
        }, 1000);
        
        this.setState({ timer });
    }
    
    componentWillUnmount() {
        console.log('componentWillUnmount - 组件即将卸载');
        
        // 清理副作用
        if (this.state.timer) {
            clearInterval(this.state.timer);
        }
        
        // 取消网络请求
        if (this.abortController) {
            this.abortController.abort();
        }
        
        // 清理事件监听器
        window.removeEventListener('resize', this.handleResize);
    }
    
    render() {
        return <div>组件内容</div>;
    }
}

3. useEffect Hook详解

useEffect Hook:useEffect是React Hooks中用于处理副作用的Hook,它结合了componentDidMount、componentDidUpdate和componentWillUnmount的功能。

3.1 基本用法

import { useState, useEffect } from 'react';

function EffectExample() {
    const [count, setCount] = useState(0);
    
    // 每次渲染后都执行
    useEffect(() => {
        console.log('useEffect执行 - 每次渲染后');
    });
    
    // 只在挂载时执行一次
    useEffect(() => {
        console.log('useEffect执行 - 仅挂载时');
    }, []);
    
    // 当count变化时执行
    useEffect(() => {
        console.log('useEffect执行 - count变化:', count);
    }, [count]);
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                增加
            </button>
        </div>
    );
}

3.2 清理副作用

function CleanupExample() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        console.log('设置定时器');
        
        const timer = setInterval(() => {
            setCount(prevCount => prevCount + 1);
        }, 1000);
        
        // 返回清理函数
        return () => {
            console.log('清理定时器');
            clearInterval(timer);
        };
    }, []);
    
    return <div>Count: {count}</div>;
}

3.3 常见副作用场景

function SideEffectsExample() {
    const [data, setData] = useState(null);
    const [windowWidth, setWindowWidth] = useState(window.innerWidth);
    
    // 场景1: 数据获取
    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch('/api/data');
                const data = await response.json();
                setData(data);
            } catch (error) {
                console.error('获取数据失败:', error);
            }
        };
        
        fetchData();
    }, []);
    
    // 场景2: 订阅事件
    useEffect(() => {
        const handleResize = () => {
            setWindowWidth(window.innerWidth);
        };
        
        window.addEventListener('resize', handleResize);
        
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, []);
    
    // 场景3: 手动DOM操作
    useEffect(() => {
        document.title = '页面标题已更新';
        
        return () => {
            document.title = 'React App';
        };
    }, []);
    
    // 场景4: 定时器
    useEffect(() => {
        const timer = setTimeout(() => {
            console.log('延迟执行的操作');
        }, 1000);
        
        return () => {
            clearTimeout(timer);
        };
    }, []);
    
    return (
        <div>
            <p>窗口宽度: {windowWidth}px</p>
            {data && <p>数据: {JSON.stringify(data)}</p>}
        </div>
    );
}

4. useEffect的依赖数组

依赖数组的重要性:依赖数组决定了useEffect何时执行。正确设置依赖数组可以避免无限循环和不必要的重复执行。

4.1 依赖数组的使用场景

function DependencyExample() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');
    
    // 场景1: 每次渲染都执行
    useEffect(() => {
        console.log('每次渲染都执行');
    });
    
    // 场景2: 只在挂载时执行一次
    useEffect(() => {
        console.log('只在挂载时执行一次');
    }, []);
    
    // 场景3: 当count变化时执行
    useEffect(() => {
        console.log('count变化:', count);
    }, [count]);
    
    // 场景4: 当count或name变化时执行
    useEffect(() => {
        console.log('count或name变化:', count, name);
    }, [count, name]);
    
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                增加
            </button>
            <input 
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="输入姓名"
            />
        </div>
    );
}

4.2 依赖数组的常见错误

常见错误:遗漏依赖项会导致闭包陷阱,而过多的依赖会导致不必要的重新执行。

// ❌ 错误:遗漏依赖项
function BadExample() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const timer = setInterval(() => {
            // 这里永远使用的是初始值0
            setCount(count + 1);
        }, 1000);
        
        return () => clearInterval(timer);
    }, []); // count被遗漏了
}

// ✅ 正确:使用函数式更新
function GoodExample() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const timer = setInterval(() => {
            // 使用函数式更新,避免闭包陷阱
            setCount(prevCount => prevCount + 1);
        }, 1000);
        
        return () => clearInterval(timer);
    }, []);
}

5. 类组件 vs Hooks

类组件

class ClassComponent extends React.Component {
    state = { count: 0 };
    
    componentDidMount() {
        document.title = `Count: ${this.state.count}`;
    }
    
    componentDidUpdate() {
        document.title = `Count: ${this.state.count}`;
    }
    
    componentWillUnmount() {
        // 清理操作
    }
    
    render() {
        return <div>{this.state.count}</div>;
    }
}

函数组件 + Hooks

function HookComponent() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        document.title = `Count: ${count}`;
        
        return () => {
            // 清理操作
        };
    }, [count]);
    
    return <div>{count}</div>;
}

6. 性能优化相关生命周期

6.1 shouldComponentUpdate

class OptimizedComponent extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        // 只有当props或state真正改变时才更新
        return (
            this.props.value !== nextProps.value ||
            this.state.count !== nextState.count
        );
    }
    
    render() {
        return <div>{this.props.value}</div>;
    }
}

6.2 React.memo

import { memo } from 'react';

const MemoComponent = memo(function MemoComponent({ value }) {
    console.log('MemoComponent渲染');
    return <div>{value}</div>;
});

// 自定义比较函数
const CustomMemoComponent = memo(
    function CustomMemoComponent({ value }) {
        return <div>{value}</div>;
    },
    (prevProps, nextProps) => {
        // 返回true表示props相等,不需要重新渲染
        return prevProps.value === nextProps.value;
    }
);

7. 错误边界

错误边界:错误边界是React组件,可以捕获其子组件树中任何位置的JavaScript错误,记录错误,并显示备用UI。

class ErrorBoundary extends React.Component {
    state = { hasError: false, error: null };
    
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
        console.error('错误边界捕获到错误:', error, errorInfo);
        
        // 可以将错误日志发送到服务器
        logErrorToService(error, errorInfo);
    }
    
    render() {
        if (this.state.hasError) {
            return (
                <div>
                    <h2>出错了!</h2>
                    <p>{this.state.error.message}</p>
                    <button onClick={() => window.location.reload()}>
                        刷新页面
                    </button>
                </div>
            );
        }
        
        return this.props.children;
    }
}

// 使用错误边界
function App() {
    return (
        <ErrorBoundary>
            <SomeComponent />
        </ErrorBoundary>
    );
}

💡 动手练习

任务:创建一个实时时钟组件

要求:

  • 显示当前时间(时:分:秒)
  • 每秒更新一次
  • 组件卸载时清理定时器
  • 添加开始/暂停功能
  • 使用useEffect Hook实现
// 参考答案
function Clock() {
    const [time, setTime] = useState(new Date());
    const [isRunning, setIsRunning] = useState(true);
    
    useEffect(() => {
        if (!isRunning) return;
        
        const timer = setInterval(() => {
            setTime(new Date());
        }, 1000);
        
        return () => clearInterval(timer);
    }, [isRunning]);
    
    const formatTime = (date) => {
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    };
    
    return (
        <div>
            <h2>当前时间</h2>
            <p style={{ fontSize: '48px', fontFamily: 'monospace' }}>
                {formatTime(time)}
            </p>
            <button onClick={() => setIsRunning(!isRunning)}>
                {isRunning ? '暂停' : '开始'}
            </button>
        </div>
    );
}

📝 关键概念总结

  • 生命周期:组件从创建到销毁的整个过程
  • 三个阶段:挂载、更新、卸载
  • useEffect:函数组件中处理副作用的Hook
  • 依赖数组:控制useEffect的执行时机
  • 清理副作用:在useEffect返回的清理函数中执行
  • 错误边界:捕获子组件树中的错误