我对 React V16.4 生命周期的理解

网上有不少关于 React 生命周期的文章,我也看了很多,为了梳理并加深我对此的理解,因此决定写这篇文章。本文主要梳理目前最新的 V16.4 的生命周期函数。如今 React 最新版本是 16.13,可是生命周期最新版本是 16.4,以后版本的生命周期没有过改动了,本文不涉及 Hooks。html

先上示意图:前端

React 生命周期示意图

废弃三个旧的生命周期函数

React 在 V16.3 版本中,为下面三个生命周期函数加上了 UNSAFEreact

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

标题中的废弃不是指真的废弃,只是不建议继续使用,并表示在 V17.0 版本中正式删除。先来讲说 React 为何要这么作。算法

主要是这些生命周期方法常常被误用和滥用。而且在 React V16.0 以前,React 是同步渲染的,而在 V16.0 以后 React 更新了其渲染机制,是经过异步的方式进行渲染的,在 render 函数以前的全部函数都有可能被执行屡次。数组

长期以来,原有的生命周期函数老是会诱惑开发者在 render 以前的生命周期函数中作一些动做,如今这些动做还放在这些函数中的话,有可能会被调用屡次,这确定不是咱们想要的结果。浏览器

废弃 UNSAFE_componentWillMount 的缘由

有一个常见的问题,有人问为何不在 UNSAFE_componentWillMount 中写 AJAX 获取数据的功能,他们的观点是,UNSAFE_componentWillMountrender 以前执行,早一点执行早获得结果。可是要知道,在 UNSAFE_componentWillMount 中发起 AJAX 请求,无论多快获得结果也赶不上首次 render,数据都是要在 render 后才能到达。安全

并且 UNSAFE_componentWillMount 在服务器端渲染也会被调用到(此方法是服务端渲染惟一会调用的生命周期函数。),你确定不但愿 AJAX 请求被执行屡次,因此这样的 IO 操做放在 componentDidMount 中更合适。性能优化

尤为是在 Fiber 启用了异步渲染以后,更没有理由在 UNSAFE_componentWillMount 中进行 AJAX 请求了,由于 UNSAFE_componentWillMount 可能会被调用屡次,谁也不会但愿无谓地屡次调用 AJAX 吧。服务器

还有人会将事件监听器(或订阅)添加到 UNSAFE_componentWillMount 中,但这可能致使服务器渲染(永远不会调用 componentWillUnmount)和异步渲染(在渲染完成以前可能被中断,致使不调用 componentWillUnmount)的内存泄漏。微信

人们一般认为 UNSAFE_componentWillMountcomponentWillUnmount 是成对出现的,但这并不能保证。只有调用了 componentDidMount 以后,React 才能保证稍后调用 componentWillUnmount 进行清理。所以,添加监听器/订阅的推荐方法是使用 componentDidMount 生命周期。

废弃 UNSAFE_componentWillReceiveProps 的缘由

有时候组件在 props 发生变化时会产生反作用。与 UNSAFE_componentWillUpdate 相似,UNSAFE_componentWillReceiveProps 可能在一次更新中被屡次调用。所以,避免在此方法中产生反作用很是重要。相反,应该使用 componentDidUpdate,由于它保证每次更新只调用一次。

UNSAFE_componentWillReceiveProps 是考虑到由于父组件引起渲染可能要根据 props 更新 state 的须要而设立的。新的 getDerivedStateFromProps 实际上与 componentDidUpdate 一块儿取代了之前的 UNSAFE_componentWillReceiveProps 函数。

废弃 UNSAFE_componentWillUpdate 的缘由

有些人使用 UNSAFE_componentWillUpdate 是出于一种错误的担忧,即当 componentDidUpdate 触发时,更新其余组件的 state 已经”太晚”了。事实并不是如此。React 可确保在用户看到更新的 UI 以前,刷新在 componentDidMountcomponentDidUpdate 期间发生的任何 setState 调用。

一般,最好避免这样的级联更新。固然在某些状况下,这些更新也是必需的(例如:若是你须要在测量渲染的 DOM 元素后,定位工具的提示)。无论怎样,在异步模式下使用 UNSAFE_componentWillUpdate 都是不安全的,由于外部回调可能会在一次更新中被屡次调用。相反,应该使用 componentDidUpdate 生命周期,由于它保证每次更新只调用一次。

大多数开发者使用 UNSAFE_componentWillUpdate 的场景是配合 componentDidUpdate,分别获取 rerender 先后的视图状态,进行必要的处理。但随着 React 新的 suspensetime slicing、异步渲染等机制的到来,render 过程能够被分割成屡次完成,还能够被暂停甚至回溯,这致使 UNSAFE_componentWillUpdatecomponentDidUpdate 执行先后可能会间隔很长时间,足够使用户进行交互操做更改当前组件的状态,这样可能会致使难以追踪的 BUG。

React 新增的 getSnapshotBeforeUpdate 方法就是为了解决上述问题,由于 getSnapshotBeforeUpdate 方法是在 UNSAFE_componentWillUpdate 后(若是存在的话),在 React 真正更改 DOM 前调用的,它获取到组件状态信息更加可靠。

除此以外,getSnapshotBeforeUpdate 还有一个十分明显的好处:它调用的结果会做为第三个参数传入 componentDidUpdate,避免了 UNSAFE_componentWillUpdate 和 componentDidUpdate 配合使用时将组件临时的状态数据存在组件实例上浪费内存,getSnapshotBeforeUpdate 返回的数据在 componentDidUpdate 中用完即被销毁,效率更高。

更多问题详见:

新增两个生命周期函数

React V16.3 中在废弃(这里的废弃不是指真的废弃,只是不建议继续使用,并表示在 V17.0 版本中正式删除)三个旧的生命周期函数的同时,React 还新增了两个生命周期函数:

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate

在 React V16.3 版本中加入的 static getDerivedStateFromProps 生命周期函数存在一个问题,就是在生命周期的更新阶段只有在 props 发生变化的时候才会调用 static getDerivedStateFromProps,而在调用了 setStateforceUpdate 时则不会。

React 官方也发现了这个问题,并在 React V16.4 版本中进行了修复。也就是说在更新阶段中,接收到新的 props,调用了 setStateforceUpdate 时都会调用 static getDerivedStateFromProps。具体在下面讲到这个函数的时候有详细说明。

React 生命周期梳理

React 生命周期主要分为三个阶段:

  • 挂载阶段
  • 更新阶段
  • 卸载阶段

挂载阶段

挂载阶段也能够理解为初始化阶段,也就是把咱们的组件插入到 DOM 中。这个阶段的过程以下:

  • constructor
  • getDerivedStateFromProps
  • UNSAVE_componentWillMount
  • render
  • (React Updates DOM and refs)
  • componentDidMount

constructor

组件的构造函数,第一个被执行。若是在组件中没有显示定义它,则会拥有一个默认的构造函数。若是咱们显示定义构造函数,则必须在构造函数第一行执行 super(props),不然咱们没法在构造函数里拿到 this,这些都属于 ES6 的知识。

在构造函数中,咱们通常会作两件事:

  • 初始化 state
  • 对自定义方法进行 this 的绑定
constructor(props) {
    super(props);

    this.state = {
      width,
      height: 'atuo',
    }

    this.handleChange1 = this.handleChange1.bind(this);
    this.handleChange2 = this.handleChange2.bind(this);
}
复制代码

getDerivedStateFromProps

使用方式:

//static getDerivedStateFromProps(nextProps, prevState)

class Example extends React.Component {
  static getDerivedStateFromProps(props, state) {
    //根据 nextProps 和 prevState 计算出预期的状态改变,返回结果会被送给 setState
    // ...
  }
}
复制代码

新的 getDerivedStateFromProps 是一个静态函数,因此不能在这函数里使用 this,简单来讲就是一个纯函数。也代表了 React 团队想经过这种方式防止开发者滥用这个生命周期函数。每当父组件引起当前组件的渲染过程时,getDerivedStateFromProps 会被调用,这样咱们有一个机会能够根据新的 props 和当前的 state 来调整新的 state

这个函数会返回一个对象用来更新当前的 state,若是不须要更新能够返回 null。这个生命周期函数用得比较少,主要用于在从新渲染期间手动对滚动位置进行设置等场景中。该函数会在挂载时,在更新时接收到新的 props,调用了 setStateforceUpdate 时被调用。

getDerivedStateFromProps

新的 getDerivedStateFromProps 实际上与 componentDidUpdate 一块儿取代了之前的 UNSAFE_componentWillReceiveProps 函数。UNSAFE_componentWillReceiveProps 也是考虑到由于父组件引起渲染可能要根据 props 更新 state 的须要而设立的。

UNSAVE_componentWillMount

UNSAFE_componentWillMount() 在挂载以前被调用。它在 render() 以前调用,所以在此方法中同步调用 setState() 不会触发额外渲染。一般,咱们建议使用 constructor() 来初始化 state。避免在此方法中引入任何反作用或订阅。如遇此种状况,请改用 componentDidMount()

此方法是服务端渲染惟一会调用的生命周期函数。UNSAFE_componentWillMount() 经常使用于当支持服务器渲染时,须要同步获取数据的场景。

render

这是 React 中最核心的方法,class 组件中惟一必须实现的方法。

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

  • 原生的 DOM,如 div
  • React 组件
  • 数组或 Fragment
  • Portals(插槽)
  • 字符串和数字,被渲染成文本节点
  • Boolean 或 null,不会渲染任何东西

render() 函数应该是一个纯函数,里面只作一件事,就是返回须要渲染的东西,不该该包含其它的业务逻辑,如数据请求,对于这些业务逻辑请移到 componentDidMountcomponentDidUpdate 中。

componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)当即调用。依赖于 DOM 节点的初始化应该放在这里。如需经过网络请求获取数据,此处是实例化请求的好地方。这个方法是比较适合添加订阅的地方。若是添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你能够在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕以前。如此保证了即便在 render() 两次调用的状况下,用户也不会看到中间状态。

请谨慎使用该模式,由于它会致使性能问题。一般,你应该在 constructor() 中初始化 state。若是你的渲染依赖于 DOM 节点的大小或位置,好比实现 modalstooltips 等状况下,你可使用此方式处理

更新阶段

更新阶段是指当组件的 props 发生了改变,或组件内部调用了 setState 或者发生了 forceUpdate,则进行更新。

这个阶段的过程以下:

  • UNSAFE_componentWillReceiveProps
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • UNSAFE_componentWillUpdate
  • render
  • getSnapshotBeforeUpdate
  • (React Updates DOM and refs)
  • componentDidUpdate

UNSAFE_componentWillReceiveProps

UNSAFE_componentWillReceiveProps 是考虑到由于父组件引起渲染可能要根据 props 更新 state 的须要而设立的。UNSAFE_componentWillReceiveProps 会在已挂载的组件接收新的 props 以前被调用。若是你须要更新状态以响应 prop 更改(例如,重置它),你能够比较 this.propsnextProps 并在此方法中使用 this.setState() 执行 state 转换。

若是父组件致使组件从新渲染,即便 props 没有更改,也会调用此方法。若是只想处理更改,请确保进行当前值与变动值的比较。在挂载过程当中,React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 一般不会触发 UNSAFE_componentWillReceiveProps()

getDerivedStateFromProps

这个方法在挂载阶段已经讲过了,这里再也不赘述。记住该函数会在挂载时,在更新时接收到新的 props,调用了 setStateforceUpdate 时被调用。它与 componentDidUpdate 一块儿取代了之前的 UNSAFE_componentWillReceiveProps 函数。

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  //...
}
复制代码

它有两个参数,根据此函数的返回值来判断是否进行从新渲染,true 表示从新渲染,false 表示不从新渲染,默认返回 true。注意,首次渲染或者当咱们调用 forceUpdate 时并不会触发此方法。此方法仅用于性能优化。

由于默认是返回 true,也就是只要接收到新的属性和调用了 setState 都会触发从新的渲染,这会带来必定的性能问题,因此咱们须要将 this.propsnextProps 以及 this.statenextState 进行比较来决定是否返回 false,来减小从新渲染,以优化性能。请注意,返回 false 并不会阻止子组件在 state 更改时从新渲染。

可是官方提倡咱们使用内置的 PureComponent 来减小从新渲染的次数,而不是手动编写 shouldComponentUpdate 代码。PureComponent 内部实现了对 props 和 state 进行浅层比较。

若是 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()。官方说在后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,而且,当返回 false 时,仍可能致使组件从新渲染。

UNSAFE_componentWillUpdate

当组件收到新的 propsstate 时,会在渲染以前调用 UNSAFE_componentWillUpdate()。使用此做为在更新发生以前执行准备更新的机会。初始渲染不会调用此方法。可是你不能此方法中调用 this.setState()。在 UNSAFE_componentWillUpdate() 返回以前,你也不该该执行任何其余操做(例如,dispatch Redux 的 action)触发对 React 组件的更新。

一般,此方法能够替换为 componentDidUpdate()。若是你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则能够将此逻辑移至 getSnapshotBeforeUpdate() 中。

render

这个方法在挂载阶段已经讲过了,这里再也不赘述。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState) {
  //...
}
复制代码

getSnapshotBeforeUpdate 生命周期方法在 render 以后,在更新以前(如:更新 DOM 以前)被调用。给了一个机会去获取 DOM 信息,计算获得并返回一个 snapshot,这个 snapshot 会做为 componentDidUpdate 的第三个参数传入。若是你不想要返回值,请返回 null,不写的话控制台会有警告。

而且,这个方法必定要和 componentDidUpdate 一块儿使用,不然控制台也会有警告。getSnapshotBeforeUpdatecomponentDidUpdate 一块儿,这个新的生命周期涵盖过期的 UNSAFE_componentWillUpdate 的全部用例。

getSnapshotBeforeUpdate(prevProps, prevState) {
  console.log('#enter getSnapshotBeforeUpdate');
  return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
  console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
复制代码

上面这段代码能够看出来这个 snapshot 怎么个用法,snapshot 乍一看还觉得是组件级别的某个“快照”,其实能够是任何值,到底怎么用彻底看开发者本身,getSnapshotBeforeUpdatesnapshot 返回,而后 DOM 改变,而后 snapshot 传递给 componentDidUpdate

官方给了一个例子,用 getSnapshotBeforeUpdate 来处理 scroll,而且说明了一般不须要这个函数,只有在从新渲染过程当中手动保留滚动位置等状况下很是有用,因此大部分开发者都用不上,也就不要乱用。

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot) {
  //...
}
复制代码

componentDidUpdate() 会在更新后会被当即调用。首次渲染不会执行此方法。在这个函数里咱们能够操做 DOM,和发起服务器请求,还能够 setState,可是注意必定要用 if 语句控制,不然会致使无限循环。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}
复制代码

若是组件实现了 getSnapshotBeforeUpdate() 生命周期,则它的返回值将做为 componentDidUpdate() 的第三个参数 snapshot 参数传递。不然此参数将为 undefined。

卸载阶段

卸载阶段,这个阶段的生命周期函数只有一个:

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁以前直接调用。咱们能够在此方法中执行必要的清理操做,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中建立的订阅等。注意不要在这个函数里调用 setState(),由于组件不会从新渲染了。

其余不经常使用的生命周期函数

还有两个很不经常使用的生命周期函数,在这也列一下。

详细使用示例请见:React 官方文档

static getDerivedStateFromError()

static getDerivedStateFromError(error) {
  //...
}
复制代码

今生命周期会在后代组件抛出错误后被调用。它将抛出的错误做为参数,并返回一个值以更新 stategetDerivedStateFromError() 会在渲染阶段调用,所以不容许出现反作用。如遇此类状况,请改用 componentDidCatch()

componentDidCatch()

componentDidCatch(error, info) {
  //...
}
复制代码

今生命周期在后代组件抛出错误后被调用。它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有 componentStack key 的对象,其中包含有关组件引起错误的栈信息。

componentDidCatch() 会在“提交”阶段被调用,所以容许执行反作用。它应该用于记录错误之类的状况:

若是发生错误,你能够经过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在将来的版本中将不推荐这样作。可使用静态 getDerivedStateFromError() 来处理降级渲染。

参考资料

本文参考了如下文章和官方文档,推荐阅读。

结语

有人会说,如今都 Hooks 一把梭了,你总结整合这些内容有啥用。其实学习这些内容,可以帮助你加深对 React 的理解,深刻领会 React 的思想。而且,目前 Class component 与 Hooks 是并存的,虽然新项目通常都直接用 Hooks,可是老项目中不免会遇到 Class component,因此仍是要学会的。


更多精彩内容,微信扫码关注公众号「技术漫谈」:

  • LeetCode 算法题解
  • JavaScript 入门到进阶
  • 前端项目从零到一实战
  • ……

相关文章
相关标签/搜索