Skip to content

组件范式与生命周期

Class 组件 vs 函数组件的演变与生命周期对应关系

1. 组件范式演变

1.1 两种组件写法

jsx
// Class 组件 (React 早期)
class Counter extends React.Component {
    state = { count: 0 };
    
    increment = () => {
        this.setState({ count: this.state.count + 1 });
    };
    
    render() {
        return (
            <button onClick={this.increment}>
                Count: {this.state.count}
            </button>
        );
    }
}

// 函数组件 + Hooks (React 16.8+)
function Counter() {
    const [count, setCount] = useState(0);
    
    return (
        <button onClick={() => setCount(count + 1)}>
            Count: {count}
        </button>
    );
}

1.2 为什么推荐函数组件?

对比项Class 组件函数组件
代码量多(constructor, bind)
this 绑定需要处理无 this
逻辑复用HOC/Render Props自定义 Hook
学习曲线陡(OOP 概念)平缓
优化shouldComponentUpdateReact.memo
未来支持维护模式主推

2. Class 组件生命周期

2.1 生命周期图解

┌─────────────────────────────────────────────────────────────────┐
│                      挂载阶段 (Mounting)                         │
├─────────────────────────────────────────────────────────────────┤
│  constructor() → static getDerivedStateFromProps() → render()   │
│       → componentDidMount()                                     │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                      更新阶段 (Updating)                         │
├─────────────────────────────────────────────────────────────────┤
│  static getDerivedStateFromProps() → shouldComponentUpdate()    │
│       → render() → getSnapshotBeforeUpdate()                    │
│       → componentDidUpdate()                                    │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                      卸载阶段 (Unmounting)                       │
├─────────────────────────────────────────────────────────────────┤
│  componentWillUnmount()                                         │
└─────────────────────────────────────────────────────────────────┘

2.2 各生命周期方法

javascript
class MyComponent extends React.Component {
    // ========== 挂载阶段 ==========
    
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        // 初始化 state,绑定方法
    }
    
    static getDerivedStateFromProps(props, state) {
        // 根据 props 更新 state(很少用)
        // 返回新 state 或 null
        if (props.value !== state.prevValue) {
            return { derivedValue: props.value, prevValue: props.value };
        }
        return null;
    }
    
    componentDidMount() {
        // DOM 已挂载,可以:
        // - 发起网络请求
        // - 添加订阅/监听器
        // - 操作 DOM
    }
    
    // ========== 更新阶段 ==========
    
    shouldComponentUpdate(nextProps, nextState) {
        // 返回 false 阻止渲染(性能优化)
        return nextProps.id !== this.props.id;
    }
    
    getSnapshotBeforeUpdate(prevProps, prevState) {
        // DOM 更新前调用,返回值传给 componentDidUpdate
        // 用于保存滚动位置等
        return this.listRef.scrollHeight;
    }
    
    componentDidUpdate(prevProps, prevState, snapshot) {
        // DOM 已更新
        // snapshot 是 getSnapshotBeforeUpdate 的返回值
        if (prevProps.id !== this.props.id) {
            this.fetchData(this.props.id);
        }
    }
    
    // ========== 卸载阶段 ==========
    
    componentWillUnmount() {
        // 清理工作:
        // - 取消订阅
        // - 清除定时器
        // - 取消网络请求
    }
    
    // ========== 错误处理 ==========
    
    static getDerivedStateFromError(error) {
        // 渲染降级 UI
        return { hasError: true };
    }
    
    componentDidCatch(error, info) {
        // 记录错误信息
        logErrorToService(error, info.componentStack);
    }
    
    render() {
        return <div>{this.state.count}</div>;
    }
}

2.3 已废弃的生命周期

javascript
// ❌ React 17+ 已废弃,会有警告
componentWillMount()        // → constructor + componentDidMount
componentWillReceiveProps() // → getDerivedStateFromProps
componentWillUpdate()       // → getSnapshotBeforeUpdate

3. 函数组件与 Hooks 对应关系

3.1 生命周期映射

Class 生命周期函数组件 Hooks
constructoruseState 初始化
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [deps])
componentWillUnmountuseEffect 返回的清理函数
shouldComponentUpdateReact.memo + useMemo
getDerivedStateFromProps渲染时直接计算
getSnapshotBeforeUpdateuseLayoutEffect

3.2 对应示例

jsx
// Class 组件
class Example extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }
    
    componentDidMount() {
        document.title = `Count: ${this.state.count}`;
        this.subscription = subscribe();
    }
    
    componentDidUpdate(prevProps, prevState) {
        if (prevState.count !== this.state.count) {
            document.title = `Count: ${this.state.count}`;
        }
    }
    
    componentWillUnmount() {
        this.subscription.unsubscribe();
    }
    
    render() {
        return <div>{this.state.count}</div>;
    }
}

// 函数组件等价实现
function Example() {
    const [count, setCount] = useState(0);
    
    // componentDidMount + componentDidUpdate
    useEffect(() => {
        document.title = `Count: ${count}`;
    }, [count]);
    
    // componentDidMount + componentWillUnmount
    useEffect(() => {
        const subscription = subscribe();
        return () => subscription.unsubscribe();  // 清理
    }, []);
    
    return <div>{count}</div>;
}

3.3 useEffect 执行时机

渲染 → DOM 更新 → 浏览器绘制 → useEffect 执行

                         异步,不阻塞渲染

渲染 → DOM 更新 → useLayoutEffect 执行 → 浏览器绘制

             同步,阻塞渲染

4. StrictMode

4.1 作用

jsx
<React.StrictMode>
    <App />
</React.StrictMode>

StrictMode 在开发模式下会:

  1. 双重调用函数组件、生命周期方法(检测副作用)
  2. 警告使用废弃 API
  3. 检测意外的副作用

4.2 为什么双重调用?

jsx
// StrictMode 会调用两次,帮助发现问题
function Counter() {
    const [count, setCount] = useState(0);
    
    // ❌ 错误:副作用在渲染中
    globalCounter++;  // 会被调用两次!
    
    // ✅ 正确:副作用在 useEffect 中
    useEffect(() => {
        globalCounter++;
    }, []);
    
    return <div>{count}</div>;
}

4.3 React 18 的变化

jsx
// React 18 StrictMode 模拟组件卸载/重新挂载
// 用于测试 Effects 的清理逻辑

useEffect(() => {
    console.log('mount');
    return () => console.log('unmount');
}, []);

// 开发模式下输出:
// mount
// unmount  (模拟卸载)
// mount    (模拟重新挂载)

5. 组件通信

5.1 Props 向下传递

jsx
// 父组件
function Parent() {
    const [value, setValue] = useState('hello');
    return <Child value={value} onChange={setValue} />;
}

// 子组件
function Child({ value, onChange }) {
    return <input value={value} onChange={e => onChange(e.target.value)} />;
}

5.2 状态提升

jsx
// 多个子组件共享状态,提升到共同父组件
function Parent() {
    const [temperature, setTemperature] = useState(0);
    
    return (
        <>
            <CelsiusInput temp={temperature} onChange={setTemperature} />
            <FahrenheitInput temp={temperature} onChange={setTemperature} />
        </>
    );
}

5.3 Context 跨层级

jsx
const ThemeContext = createContext('light');

function App() {
    return (
        <ThemeContext.Provider value="dark">
            <DeepChild />
        </ThemeContext.Provider>
    );
}

function DeepChild() {
    const theme = useContext(ThemeContext);
    return <div className={theme}>...</div>;
}

6. 面试高频问题

Q1: 函数组件和 Class 组件的区别?

区别Class 组件函数组件
写法ES6 class普通函数
状态this.stateuseState
生命周期生命周期方法useEffect
this需要绑定无 this
性能实例化开销更轻量
逻辑复用HOC/Render Props自定义 Hook

Q2: useEffect 和 componentDidMount 的区别?

区别componentDidMountuseEffect
执行时机DOM 挂载后同步执行DOM 挂载后异步执行
依赖无依赖概念可指定依赖
清理componentWillUnmount返回清理函数
数量一个可以多个

Q3: 为什么 useEffect 的依赖数组很重要?

jsx
// 依赖数组控制 effect 何时执行
useEffect(() => {}, []);        // 只在挂载时执行
useEffect(() => {}, [dep]);     // dep 变化时执行
useEffect(() => {});            // 每次渲染都执行(通常是 bug)

Q4: StrictMode 为什么会导致双重渲染?

为了帮助发现:

  1. 非幂等的渲染逻辑
  2. 副作用问题
  3. 状态初始化问题

通过双重调用,可以发现那些依赖"只执行一次"假设的 bug。

Q5: getDerivedStateFromProps 什么时候用?

很少用。大多数场景可以用:

  1. 完全受控组件(状态由父组件管理)
  2. 完全非受控组件 + key(重置时用新 key)
  3. useEffect 中更新状态
jsx
// 避免使用 getDerivedStateFromProps
// ✅ 更好的方式:使用 key 重置组件
<EmailInput key={userId} defaultEmail={user.email} />

前端面试知识库