React的生命周期都懂了吗?

配图源自 Freepik

发现好像有些没有过的生命周期函数,还没完全弄清楚...

一、组件的生命周期

组件的生命周期,主要分为 Mounting(挂载)、Updating(更新)、Unmounting(卸载)三个阶段。

React ≥ 16.4
Mounting

当组件示例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

以下生命周期方法即将过时,在新代码中应该避免使用它们:UNSAFE_componentWillMount()

Updating

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

请注意,以下方法即将过时,在新代码中应该避免使用它们:UNSAFE_componentWillUpdate()UNSAFE_componentWillReceiveProps()

Unmounting

当组件从 DOM 中移除时会调用如下方法:

  • componentWillUnmount()
Error Handling(错误处理)

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError()
  • componentDidCatch()

二、Mounting(挂载)

2.1 constructor
constructor(props)

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应该在其他语句之前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

一般需重写构造函数,只做两件事:

  • 初始化组件内部 state,即 this.state = { ... }
  • 为时间处理函数绑定实例,如:this.handleClick = this.handleClick.bind(this)

否则无需为 React 组件实现构造函数。

2.2 static getDerivedStateFromProps(不常用)
static getDerivedStateFromProps(props, state)

需要注意的是,此方法无法访问组件实例,即不能使用 this

getDerivedStateFromState 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个 对象 来更新 state,如果返回 null 则不更新任何内容。

componentWillReceiveProps 不同的是,getDerivedStateFromProps 不管原因是什么,都会在每次渲染前触发此方法。而 componentWillReceiveProps 仅在父组件重新渲染时触发,而不是在内部调用 setState 时。

请避免使用派生 state。

2.2.1 UNSAFE_componentWillReceiveProps

此生命周期之前名为 componentWillReceiveProps。该名称将继续使用至 React 17。在 React 16.3 之后,它的替代者是 getDerivedStateFromProps

UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更新(例如,重置它),你可以比较 this.propsnextProps 并在此方法中使用 this.setState() 执行 state 转换。

请注意,如果父组件导致组件重新渲染,即使没有 props 没有更改,也会调用此方法。如果只是想处理更改,请确保进行当前值与变更值的比较。

在挂载过程,React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发 UNSAFE_componentWillReceiveProps()

2.3 UNSAFE_componentWillMount

该生命周期之前名为 componentWillMount,旧名称仍可使用至 React 17.

UNSAFE_componentWillMount()constructor 之后,render 之前被调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。

避免在此方法引入任何副作用或订阅,请放置在 componentDidMount()

此方法是服务端渲染唯一会调用的生命周期函数。

2.4 render

render() 方法是 class 组件中唯一必须实现的方法。

render 被调用,它会检测 this.propsthis.state 的变化并返回以下类型之一:

  • React 元素:通常通过 JSX 创建。例如,
    会被 React 渲染为 DOM 节点, 会被 React 渲染为自定义组件,无论是
    还是 均为 React 元素。
  • 数组或 fragments: 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
  • Portals:可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
  • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
  • 布尔类型或 null:什么都不渲染。(主要用于支持返回 test && 的模式,其中 test 为布尔类型。)

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保存 render() 为纯函数,可以使组件更容易思考。

需要注意都是,shouleComponentUpdate() 返回 false,则不会调用 render()

2.5 componentDidMount
componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个地方是比较合适添加订阅。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅。

你可以在 componentDidMount() 里直接调用 setState()。它将会触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 或 tooltips 等情况下,你可以使用此方式处理。

三、Updating(更新)

3.1 shouldComponentUpdate(不常用)
shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 stateprops 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

propsstate 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()。PureComponent 会对 propsstate 进行浅层比较,并减少了跳过必要更新的可能性。

如果你一定要手写编写此函数,可以将 this.propsnextProps 以及 this.statenextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()。后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且当返回 false 时,仍可能导致组件重新渲染。

3.2 UNSAFE_componentWillUpdate(不常用)
UNSAFE_componentWillUpdate(nextProps, nextState)

此生命周期之前名为 componentWillUpdate。该名称将继续使用至 React 17。

当组件收到新的 propsstate 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为更新发生之前执行准备更新的几乎。初始渲染不会调用此方法。

注意,你不能此方法中调用 setState();在 UNSAFE_componentWillUpdate() 返回之前,你不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新。

通常,此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动违章),则可以将此逻辑移至 getSnapShotBeforeUpdate() 中。

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()

3.3 componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后被立即调用,但首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 为发生变化时,则不会执行网络请求。)

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props)
  // 若直接 setState() 会导致死循环。
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID)
  }
}

你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 snapshot 传递。否则此参数将为 undefined

需要注意的是,shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()

四、Unmounting(卸载)

4.1 componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如清除 timer,取消网络请求或清除 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

需要注意的是,我们常说的组件销毁,只是组件从页面中删除而已。而并不意味着它真正被销毁,被 GC 回收。我们所写的 JSX 形式的组件,都会被 Babel 转化成 React.createElement() 形式的JS 对象(没错,React 组件本质上就是一个 JavaScript 对象)。因此只有它不再被引用,垃圾回收机制才会回收掉,这才算真正销毁。这也是为什么要做 componentWillUnmount 清除副作用的原因。

五、其他

5.1 defaultProps

defaultProps 可以为 Class 组件添加默认 props。这一般用于 props 未赋值,但又不能为 null 的情况。例如:

class CustomButton extends Component {
  // ...
}

CustomButton.defaultProps = {
  type: 'primary'
}

利用 ES6 的 class 静态语法

class CustomButton extends Component {
  static defaultProps = {
    type: 'primary'
  }
  // ...
}
5.2 一些问题

React 16 之后采用了 Fiber 架构,只有 componentDidMount 生命周期函数是确定被执行一次的,类似 ComponentWillMount 的生命周期钩子都有可能执行多次,所以不加以在这些生命周期中做有副作用的操作,比如请求数据之类。

3.3 componentDidCatch()

未完待续...

五、逐步迁移路径

React 遵循语义版本控制,因此这种变化将是逐步的。我们目前的计划是:

  • 16.3:为不安全的生命周期引入别名,UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用。)
  • 未来 16.x 版本:为 componentWillMountcomponentWillReceivePropscomponentWillUpdate 启用废弃告警。(旧的生命周期名称和新的别名都将在这个版本中工作,但是旧的名称在开发模式下会产生一个警告。)
  • 17.0:删除 componentWillMountcomponentWillReceivePropscomponentWillUpdate。(在此版本之后,只有新的 UNSAFE_ 生命周期名称可以使用。)

这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后。

六、示例

由于使用新 API(如 getDerivedStateFromPropsgetSnapshotBeforeUpdate)时,React 不会调用组件的 “unsafe” 生命周期(如 UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate),因此暂时先注释掉新 API。同时使用,会出现类似如下警告⚠️:

Warning: Unsafe legacy lifecycles will not be called for components using new component APIs.

LifeCycle uses getDerivedStateFromProps() but also contains the following legacy lifecycles:
  UNSAFE_componentWillMount

The above lifecycles should be removed. Learn more about this warning here:
https://fb.me/react-unsafe-component-lifecycles

示例基于 React 16.12.0 版本。

import React, { Component } from 'react'

export default class LifeCycle extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
    this.setCount = this.setCount.bind(this)
    console.log('---> constructor')
  }

  static getDerivedStateFromProps(prevProps, prevState) {
    console.log('---> getDerivedStateFromProps')
    return null
  }

  // 不会为使用新组件 API 的组件调用不安全的遗留生命周期。
  // Warning: Unsafe legacy lifecycles will not be called for components using new component APIs.
  // LifeCycle uses getDerivedStateFromProps() but also contains the following legacy lifecycles: UNSAFE_componentWillMount
  // UNSAFE_componentWillMount() {
  //   console.log('---> UNSAFE_component')
  // }

  componentDidMount() {
    console.log('---> componentDidMount')
  }

  setCount() {
    this.setState({ count: this.state.count + 1 })
  }

  render() {
    console.log('---> render')

    return (
      
Count: {this.state.count}
) } }

当首次加载加载时,分别触发了以下生命周期,打印结果:

---> constructor
---> UNSAFE_componentWillMount
---> render
---> componentDidMount

当我们点击按钮通过 setState 更新 count 时,会触发以下动作:

---> shouldComponentUpdate
---> UNSAFE_componentWillUpdate
---> render
---> componentDidUpdate

未完待续...

七、参考

  • React Lifecycle Methods diagram
  • 你真的了解 React 生命周期吗
  • 检测意外的副作用

你可能感兴趣的:(React的生命周期都懂了吗?)