前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

React 16.3 新生命周期详解与旧钩子废弃原因

首页2018-11-18 23:30:12Front-End
React生命周期getDerivedStateFromPropsReact 原理

接手一个老项目,yarn start 之后控制台刷一屏黄字,说 componentWillReceiveProps 已经改名成 UNSAFE_componentWillReceiveProps。当时最直接的反应是把名字改一下把警告压下去,改完发现还是有地方在报,而且顺着报错点进去看,那段逻辑是「拿到新 props 就发一次请求」。这个写法在 React 15 时代到处都是,凭什么现在就成了 unsafe 的?

这篇就是顺着这个问题往下挖的记录。先把 React 16.0 之前那套完整的生命周期讲一遍,再讲 Fiber 之后为什么必须动刀,最后把 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 这两个新钩子的适用边界说清楚。读完你应该能回答一个面试里很常见的问题:为什么 shouldComponentUpdate 活下来了,它旁边那三个却没有。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • React 16.0 之前完整的四个阶段和每个钩子的职责
  • 造成组件更新的两类三种情况,以及各自怎么优化
  • Fiber 的可中断渲染为什么会让 render 之前的钩子变得危险
  • React 16.3 和 16.4 的 getDerivedStateFromProps 有什么不一样
  • getSnapshotBeforeUpdate 解决的是哪一类问题
  • 派生 state 这件事本身的坑,以及官方推荐的替代写法
  • 这套 API 到今天(React 19 时代)的现状

# 一、React v16.0 前的生命周期

先看这张全景图,后面所有讨论都会回到它上面。

React v16.0 之前的完整生命周期流程图

整张图分成四段,初始化、挂载、更新、卸载。挂载和卸载各走一次,更新这一段会反复走。

# 1.1 第一个是组件初始化(initialization)阶段

也就是以下代码中类的构造方法(constructor())。Test 类继承了 react 的 Component 这个基类,正因为继承了这个基类,才能有 render()、生命周期等方法可以使用,这也说明了为什么函数组件不能使用这些方法。

super(props) 用来调用基类的构造方法,也将父组件的 props 注入给子组件供子组件读取(组件中 props 只读不可变,state 可变),而 constructor() 用来做一些组件的初始化工作,比如定义 this.state 的初始内容。

import React, { Component } from 'react';

class Test extends Component {
  constructor(props) {
    super(props);
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码本身没干什么活,但它是理解后面一切的起点。构造函数是整个组件生命里唯一一次「什么都还没有」的时刻,DOM 没有,props 的后续变化也还没发生。所以凡是「只需要算一次的初始值」,放这里最省事。

顺带说一句,如果构造函数里只有 super(props) 这一行,那这个 constructor 可以整个删掉,ES6 class 会自动生成。ESLint 的 no-useless-constructor 规则就管这个。

# 1.2 第二个是组件的挂载(Mounting)阶段

这个阶段依次经过 componentWillMount、render、componentDidMount 三步。

# 1.2.1 componentWillMount

在组件挂载到 DOM 前调用,且只会被调用一次。在这里调用 this.setState 不会引起组件重新渲染,也可以把写在这里的内容提前到 constructor() 中,所以项目中很少用。

这条「可以提前到 constructor」的结论很关键,它其实已经暗示了这个钩子存在的必要性不强,后面 React 团队砍掉它的底气也在这里。

# 1.2.2 render

根据组件的 props 和 state(无两者的重传递和重赋值,无论值是否有变化,都可以引起组件重新 render),return 一个 React 元素(描述组件,即 UI),不负责组件实际渲染工作,之后由 React 自身根据此元素去渲染出页面 DOM。

render 是纯函数(Pure function,函数的返回结果只依赖于它的参数,函数执行过程里面没有副作用),不能在里面执行 this.setState,会有改变组件状态的副作用。

这里的「纯」不是一句道德倡议,是硬约束。你在 render 里写一次埋点上报,Fiber 时代这段代码可能被跑两遍,埋点数就翻倍了。

# 1.2.3 componentDidMount

组件挂载到 DOM 后调用,且只会被调用一次。

这是发请求、加事件监听、初始化第三方图表实例的标准位置。原因很简单,走到这一步真实 DOM 已经在页面上了,this.refs 和 ref 都能拿到节点。

# 1.3 第三个是组件的更新(update)阶段

setState 引起的 state 更新,或父组件重新 render 引起的 props 更新,更新后的 state 和 props 相对之前无论是否有变化,都将引起子组件的重新 render。

那句「无论是否有变化」是很多性能问题的源头,下面拆开讲。

# 1.3.1 造成组件更新有两类(三种)情况

1. 父组件重新 render

父组件重新 render 引起子组件重新 render 的情况有两种。

a. 直接使用,每当父组件重新 render 导致的重传 props,子组件将直接跟着重新渲染,无论 props 是否有变化。可通过 shouldComponentUpdate 方法优化。

class Child extends Component {
   shouldComponentUpdate(nextProps){ // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
        if(nextProps.someThings === this.props.someThings){
          return false
        }
    }
    render() {
        return <div>{this.props.someThings}</div>
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码在解决的是「父组件动一下,子树全体陪跑」的问题。要注意这个示例只在相等时 return false,不相等时函数走到底返回 undefined,React 会当成真值继续渲染,行为上是对的,但更严谨的写法是显式 return true。生产代码里这类隐式返回很容易在后面加分支时出错。

b. 在 componentWillReceiveProps 方法中,将 props 转换成自己的 state。

class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            someThings: props.someThings
        };
    }
    componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
        this.setState({someThings: nextProps.someThings});
    }
    render() {
        return <div>{this.state.someThings}</div>
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染。

原因是 componentWillReceiveProps 中判断 props 是否变化了,若变化了,this.setState 将引起 state 变化,从而引起 render,此时就没必要再做第二次因重传 props 引起的 render 了,不然重复做一样的渲染了。

这个写法有个正式名字叫「派生 state」,它是整篇文章里最值得警惕的一处,第四节会专门算这笔账。

2. 组件本身调用 setState,无论 state 有没有变化。可通过 shouldComponentUpdate 方法优化

class Child extends Component {
   constructor(props) {
        super(props);
        this.state = {
          someThings:1
        }
   }
   shouldComponentUpdate(nextProps, nextState){ // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
        if(nextState.someThings === this.state.someThings){
          return false
        }
        return true
    }

   handleClick = () => { // 虽然调用了setState ,但state并无变化
        const preSomeThings = this.state.someThings
         this.setState({
            someThings: preSomeThings
         })
   }

    render() {
        return <div onClick = {this.handleClick}>{this.state.someThings}</div>
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这里我改了原文一处签名。shouldComponentUpdate 的形参顺序固定是 (nextProps, nextState),原文写成了 shouldComponentUpdate(nextStates),那样第一个参数拿到的其实是 props,比较永远为 false,return false 分支根本进不去。这是个很典型的坑,把它当反面教材记住就行。

关于 setState 本身的批量合并、异步表现和更新队列,我在 React setState 原理剖析 里单独拆过,这里不重复。

# 1.3.2 componentWillReceiveProps(nextProps)

此方法只调用于 props 引起的组件更新过程中,参数 nextProps 是父组件传给当前组件的新 props。但父组件 render 方法的调用不能保证重传给当前组件的 props 是有变化的,所以在此方法中要根据 nextProps 和 this.props 来查明重传的 props 是否改变,以及如果改变了要执行什么,比如根据新的 props 调用 this.setState 触发当前组件的重新 render。

# 1.3.3 shouldComponentUpdate(nextProps, nextState)

此方法通过比较 nextProps、nextState 及当前组件的 this.props、this.state,返回 true 时当前组件将继续执行更新过程,返回 false 则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。

这边也可以看出,就算 componentWillReceiveProps() 中执行了 this.setState 更新了 state,但在 render 前(如 shouldComponentUpdate、componentWillUpdate),this.state 依然指向更新前的 state,不然 nextState 及当前组件的 this.state 的对比就一直是 true 了。

# 1.3.4 componentWillUpdate(nextProps, nextState)

此方法在调用 render 方法前执行,在这里可执行一些组件更新发生前的工作,一般较少用。

# 1.3.5 componentDidUpdate(prevProps, prevState)

fe
  • 一、React v16.0 前的生命周期
    • 1.1 第一个是组件初始化(initialization)阶段
    • 1.2 第二个是组件的挂载(Mounting)阶段
      • 1.2.1 componentWillMount
      • 1.2.2 render
      • 1.2.3 componentDidMount
    • 1.3 第三个是组件的更新(update)阶段
      • 1.3.1 造成组件更新有两类(三种)情况
      • 1.3.2 componentWillReceiveProps(nextProps)
      • 1.3.3 shouldComponentUpdate(nextProps, nextState)
      • 1.3.4 componentWillUpdate(nextProps, nextState)
      • 1.3.5 componentDidUpdate(prevProps, prevState)
    • 1.4 卸载阶段
  • 二、React v16.4 的生命周期
    • 2.1 变更缘由
    • 2.2 引入了两个新的生命周期函数
      • 2.2.1 getDerivedStateFromProps
      • 2.2.2 getSnapshotBeforeUpdate
  • 三、派生 state 这件事本身就有坑
  • 四、这套 API 到今天还成立吗
  • 总结
  • 参考

← pm2使用小结React性能优化总结,从render机制到memo的落地实践 →