React 新旧生命周期对比

核心分界线:React 16.3(引入 Fiber 架构)


核心变化概览

变化可概括为:废弃 3 个 will 钩子,新增 2 个钩子,保留核心流程不变。

变更类型 旧版钩子 新版替代 说明
废弃 componentWillMount --- 挂载前可能执行多次,不安全
废弃 componentWillReceiveProps getDerivedStateFromProps 用静态方法替代,避免副作用
废弃 componentWillUpdate getSnapshotBeforeUpdate 更新前 DOM 快照捕获
新增 --- getDerivedStateFromProps 静态方法,根据 props 派生 state
新增 --- getSnapshotBeforeUpdate 获取更新前 DOM 状态,返回值传给 componentDidUpdate
保留 constructor constructor 初始化状态
保留 render render 渲染 UI
保留 shouldComponentUpdate shouldComponentUpdate 性能优化阀门
保留 componentDidMount componentDidMount 挂载完成,发请求、订阅
保留 componentDidUpdate componentDidUpdate(prevProps, prevState, snapshot) 更新完成,新增 snapshot 参数
保留 componentWillUnmount componentWillUnmount 清理工作

💡 一句话总结 :旧版的 componentWillMountcomponentWillReceivePropscomponentWillUpdate 被废弃,取而代之的是 getDerivedStateFromPropsgetSnapshotBeforeUpdate。其余生命周期钩子基本保持不变。


旧版生命周期(React 16.3 之前)

挂载阶段

kotlin 复制代码
constructor → componentWillMount → render → componentDidMount

更新阶段

触发方式 执行顺序
setState() shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate
props 变化 componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate
forceUpdate() 跳过 shouldComponentUpdate,直接 componentWillUpdaterendercomponentDidUpdate

卸载阶段

复制代码
componentWillUnmount

新版生命周期(React 16.3+)

挂载阶段

kotlin 复制代码
constructor → getDerivedStateFromProps → render → componentDidMount

更新阶段

scss 复制代码
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate(prevProps, prevState, snapshot)

卸载阶段

复制代码
componentWillUnmount

新旧生命周期对比总表

生命周期 状态 旧版行为 新版行为
constructor 保留 初始化 state,绑定事件处理器 功能不变
componentWillMount 废弃 组件挂载前调用,可执行副作用 移除。副作用移至 componentDidMount
componentWillReceiveProps 废弃 props 变化时调用,可执行副作用和 setState 替换为 getDerivedStateFromProps(静态方法,无 this
getDerivedStateFromProps 新增 --- 静态方法,接收 propsstate,返回对象更新 state 或 null
shouldComponentUpdate 保留 返回 false 阻止更新 功能不变,性能优化阀门
componentWillUpdate 废弃 更新前调用,可读取 DOM 状态 替换为 getSnapshotBeforeUpdate
getSnapshotBeforeUpdate 新增 --- 更新前获取 DOM 快照,返回值传给 componentDidUpdate
render 保留 返回 JSX / null 功能不变
componentDidMount 保留 挂载完成,发请求、订阅 功能不变
componentDidUpdate 保留 接收 prevPropsprevState 新增第三个参数 snapshot(来自 getSnapshotBeforeUpdate
componentWillUnmount 保留 清理定时器、取消订阅 完全不变

为什么废弃 3 个 will 钩子?

⚠️ 根本原因:Fiber 架构的异步渲染

React 16 引入 Fiber Reconciler,将渲染拆分为可中断、可恢复的小任务单元。这导致 will 系列钩子在异步渲染中可能被多次调用,产生不可预期的副作用。

Stack Reconciler vs Fiber Reconciler

特性 Stack Reconciler(旧版) Fiber Reconciler(新版)
渲染方式 深度优先递归同步渲染 将渲染拆分为小任务单元
可中断性 一旦开始无法中断 可中断、可恢复、可优先级调度
will 钩子执行次数 只执行一次,行为确定 可能被执行多次

具体问题场景

重复副作用问题 :假设在 componentWillMount 中发起 API 请求,如果渲染被中断后重新开始,请求会被重复发送,造成资源浪费和数据不一致。

javascript 复制代码
// ❌ 危险:componentWillMount 中发请求
componentWillMount() {
    fetch('/api/data').then(res => {
        this.setState({ data: res.data });
    });
    // 如果渲染中断重启,请求会被重复发送!
}
javascript 复制代码
// ✅ 正确:componentDidMount 中发请求
componentDidMount() {
    fetch('/api/data').then(res => {
        this.setState({ data: res.data });
    });
    // 只在挂载完成时执行一次,安全
}

Fiber 架构与生命周期变更

React 团队的决策逻辑

废弃的钩子 在 Fiber 中的问题 解决方案
componentWillMount 可能被多次调用,副作用不可控 移除,副作用移至 componentDidMount
componentWillReceiveProps 可执行 setState 和副作用,在 Fiber 中行为不可预期 替换为纯函数 getDerivedStateFromProps
componentWillUpdate 可读取 DOM,但 Fiber 中 DOM 可能尚未准备好 替换为 getSnapshotBeforeUpdate 获取快照

💡 设计原则 :React 团队将生命周期分为渲染阶段 (Render Phase)和提交阶段 (Commit Phase)。will 钩子位于渲染阶段,可能被多次调用;did 钩子位于提交阶段,保证只执行一次。


getDerivedStateFromProps 详解

基本用法

javascript 复制代码
class Example extends React.Component {
    static getDerivedStateFromProps(nextProps, prevState) {
        // 纯函数:不能访问 this
        // 根据 props 派生 state
        if (nextProps.userId !== prevState.prevUserId) {
            return {
                prevUserId: nextProps.userId,
                userData: null  // 触发重新加载
            };
        }
        return null;  // 不更新 state
    }

    state = {
        prevUserId: null,
        userData: null
    };
}

与 componentWillReceiveProps 对比

🔴 componentWillReceiveProps(旧版)

javascript 复制代码
componentWillReceiveProps(nextProps) {
    // ❌ 可以访问 this
    // ❌ 可以执行副作用
    // ❌ 可以 setState
    if (nextProps.id !== this.props.id) {
        this.setState({ loading: true });
        fetchData(nextProps.id); // 副作用!
    }
}

🟢 getDerivedStateFromProps(新版)

javascript 复制代码
static getDerivedStateFromProps(nextProps, prevState) {
    // ✅ 纯函数,无 this
    // ✅ 无副作用
    // ✅ 只能返回对象或 null
    if (nextProps.id !== prevState.prevId) {
        return { prevId: nextProps.id, loading: true };
    }
    return null;
}

⚠️ 使用建议getDerivedStateFromProps 容易导致代码复杂化。官方建议优先考虑以下替代方案:

  1. 完全受控组件:用 props 直接控制,不使用 state
  2. 完全非受控组件 :使用 key 属性重置组件状态
  3. 使用 Hooks :函数组件中 useEffect 更灵活

getSnapshotBeforeUpdate 详解

典型场景:保存滚动位置

javascript 复制代码
class ChatList extends React.Component {
    getSnapshotBeforeUpdate(prevProps, prevState) {
        // 更新前记录列表底部距离
        if (prevProps.messages.length < this.props.messages.length) {
            const list = this.listRef.current;
            return list.scrollHeight - list.scrollTop;
        }
        return null;
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        // 更新后恢复滚动位置
        if (snapshot !== null) {
            const list = this.listRef.current;
            list.scrollTop = list.scrollHeight - snapshot;
        }
    }
}

执行时机

  1. render() 输出新的虚拟 DOM
  2. getSnapshotBeforeUpdate() 在 DOM 更新前执行,可读取当前 DOM 状态
  3. React 将变更提交到真实 DOM
  4. componentDidUpdate(prevProps, prevState, snapshot) 接收快照值

Hooks 替代方案(React 16.8+)

随着 Hooks 的引入,函数组件可以完全替代类组件的生命周期管理。

类组件生命周期 函数组件 Hook 说明
constructor useState / useReducer 初始化状态
componentDidMount useEffect(() => {}, []) 空依赖数组,只执行一次
componentDidUpdate useEffect(() => {}, [deps]) 指定依赖数组
componentWillUnmount useEffect(() => { return () => {} }, []) 返回清理函数
shouldComponentUpdate React.memo + useMemo / useCallback 性能优化
getDerivedStateFromProps 直接在组件体中计算派生值 或使用 useMemo
getSnapshotBeforeUpdate 自定义 Hook + ref 需手动实现

Hook 示例

javascript 复制代码
function Example({ userId }) {
    const [userData, setUserData] = useState(null);

    // 替代 componentDidMount + componentDidUpdate
    useEffect(() => {
        let cancelled = false;
        fetchUser(userId).then(data => {
            if (!cancelled) setUserData(data);
        });

        // 替代 componentWillUnmount
        return () => { cancelled = true; };
    }, [userId]); // 依赖变化时重新执行

    return <div>{userData?.name}</div>;
}

最佳实践

1. 副作用放在正确的位置

场景 推荐钩子 不推荐
初始化发请求、订阅 componentDidMount componentWillMount(废弃)
props 变化时同步 state getDerivedStateFromProps(谨慎) componentWillReceiveProps(废弃)
更新前保存 DOM 信息 getSnapshotBeforeUpdate componentWillUpdate(废弃)
性能优化(避免不必要渲染) shouldComponentUpdate / React.memo ---
清理定时器、取消订阅 componentWillUnmount ---

2. 避免常见陷阱

🚫 不要在 getDerivedStateFromProps 中执行副作用

这是一个静态方法,无法访问 this,也不能发请求或操作 DOM。它应该只用于根据 props 计算并返回新的 state。
⚠️ 谨慎使用派生 state

大多数 getDerivedStateFromProps 的使用场景都可以用更好的方式替代。在引入它之前,先问自己:这个 state 真的需要派生吗?


迁移建议

逐步迁移策略

迁移检查清单:

  1. 第一步 :识别代码中使用的 UNSAFE_ 前缀方法(如 UNSAFE_componentWillMount
  2. 第二步 :将 componentWillMount 中的逻辑移至 componentDidMount
  3. 第三步 :将 componentWillReceiveProps 替换为 getDerivedStateFromProps 或重构为完全受控/非受控组件
  4. 第四步 :将 componentWillUpdate 替换为 getSnapshotBeforeUpdate
  5. 第五步(可选):考虑将类组件重构为函数组件 + Hooks

使用 codemod 自动迁移

bash 复制代码
# React 官方提供的 codemod,自动重命名废弃的生命周期方法
npx react-codemod rename-unsafe-lifecycles

💡 长期建议 :对于新项目,优先使用函数组件 + Hooks。类组件的生命周期概念在维护老代码时仍然重要,但新开发应拥抱 Hooks 带来的更简洁、更可组合的状态管理方案。

相关推荐
PBitW2 小时前
为什么vite中TS报错,可以继续运行?Webpack不行?💡
前端·typescript
GISer_Jing2 小时前
一套H5跑通三端:App+小程序内嵌H5跨端适配全栈解决方案
前端·前端框架·ai编程
码云之上2 小时前
项目团队从 5 人扩到 15 人,我写了个 CLI 让 IDE 共享 AI 规则
前端·人工智能·后端
飞天狗2 小时前
LCP / INP / CLS 三个维度的实战调优:把 Web Vitals 从不及格拉到优秀的完整路径
前端·性能优化
Csvn2 小时前
Vue 3 `<script setup>` 解构 props 后响应式丢了?一个「字段不更新」的排查实录
前端
用户298698530142 小时前
前端 Excel 处理进阶:React 中合并与取消合并单元格的实现
前端·javascript·react.js
Highcharts2 小时前
@highcharts/react完整技术指南|新版Highcharts React使用、迁移、SSR、TS 优化全解
前端·javascript
老王以为2 小时前
Fiber 节点 —— 一个数据结构如何承载整个 React 运行时
前端·react native·react.js