谈谈对 React 新旧生命周期的理解

前言

在写这篇文章的时候,React 已经出了 17.0.1 版本了,虽然说还来讨论目前 React 新旧生命周期有点晚了,React 两个新生命周期虽然出了好久,但实际开发我却没有用过,由于 React 16 版本后咱们直接 React Hook 起飞开发项目。javascript

但对新旧生命周期的探索,仍是有助于咱们更好理解 React 团队一些思想和作法,因而今天就要回顾下这个问题和理解总结,虽然仍是 React Hook 写法香,可是依然要深究学习类组件的东西,了解 React 团队的一些思想与作法。html

本文只讨论 React17 版本前的。java

React 16 版本后作了什么

首先是给三个生命周期函数加上了 UNSAFE:react

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

这里并非表示不安全的意思,它只是不建议继续使用,并表示使用这些生命周期的代码可能在将来的 React 版本(目前 React17 尚未彻底废除)存在缺陷,如 React Fiber 异步渲染的出现。git

同时新增了两个生命周期函数:github

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

UNSAFE_componentWillReceiveProps

UNSAFE_componentWillReceiveProps(nextProps)

先来讲说这个函数,componentWillReceiveProps编程

该子组件方法并非父组件 props 改变才触发,官方回答是:安全

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

先来讲说 React 为何废除该函数,废除确定有它很差的地方。服务器

componentWillReceiveProps函数的通常使用场景是:异步

  • 若是组件自身的某个 state 跟父组件传入的 props 密切相关的话,那么能够在该方法中判断先后两个 props 是否相同,若是不一样就根据 props 来更新组件自身的 state。
    相似的业务需求好比:一个能够横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但不少状况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。

但该方法缺点是会破坏 state 数据的单一数据源,致使组件状态变得不可预测,另外一方面也会增长组件的重绘次数。

而在新版本中,官方将更新 state 与触发回调从新分配到了 getDerivedStateFromPropscomponentDidUpdate 中,使得组件总体的更新逻辑更为清晰。

新生命周期方法static getDerivedStateFromProps(props, state)怎么用呢?

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

从函数名字就能够看出大概意思:使用 props 来派生/更新 state。这就是重点了,但凡你想使用该函数,都必须出于该目的,使用它才是正确且符合规范的。

getDerivedStateFromProps不一样的是,它在挂载和更新阶段都会执行(componentWillReceiveProps挂载阶段不会执行),由于更新 state 这种需求不只在 props 更新时存在,在 props 初始化时也是存在的。

并且getDerivedStateFromProps在组件自身 state 更新也会执行而componentWillReceiveProps方法执行则取决于父组件的是否触发从新渲染,也能够看出getDerivedStateFromProps并非 componentWillReceiveProps方法的替代品.

引发咱们注意的是,这个生命周期方法是一个静态方法,静态方法不依赖组件实例而存在,故在该方法内部是没法访问 this 的。新版本生命周期方法能作的事情反而更少了,限制咱们只能根据 props 来派生 state,官方是基于什么考量呢?

由于没法拿到组件实例的 this,这也致使咱们没法在函数内部作 this.fetch()请求,或者不合理的 this.setState()操做致使可能的死循环或其余反作用。有没有发现,这都是不合理不规范的操做,但开发者们都有机会这样用。可若是加了个静态 static,间接强制咱们都没法作了,也从而避免对生命周期的滥用。

React 官方也是经过该限制,尽可能保持生命周期行为的可控可预测,根源上帮助了咱们避免不合理的编程方式,即一个 API 要保持单一性,作一件事的理念。

以下例子:

// before
componentWillReceiveProps(nextProps) {
  if (nextProps.isLogin !== this.props.isLogin) {
    this.setState({
      isLogin: nextProps.isLogin,
    });
  }
  if (nextProps.isLogin) {
    this.handleClose();
  }
}

// after
static getDerivedStateFromProps(nextProps, prevState) {
  if (nextProps.isLogin !== prevState.isLogin) { // 被对比的props会被保存一份在state里
    return {
      isLogin: nextProps.isLogin, // getDerivedStateFromProps 的返回值会自动 setState
    };
  }
  return null;
}

componentDidUpdate(prevProps, prevState) {
  if (!prevState.isLogin && this.props.isLogin) {
    this.handleClose();
  }
}

UNSAVE_componentWillMount

UNSAFE_componentWillMount() 在挂载以前被调用。它在 render() 以前调用,所以在此方法中同步调用 setState() 不会触发额外渲染。

咱们应该避免在此方法中引入任何反作用或事件订阅,而是选用componentDidMount()

在 React 初学者刚接触的时候,可能有这样一个疑问:通常都是数据请求放在componentDidMount里面,但放在componentWillMount不是会更快获取数据吗?

由于理解是componentWillMount在 render 以前执行,早一点执行就早拿到请求结果;可是其实无论你请求多快,都赶不上首次 render,页面首次渲染依旧处于没有获取异步数据的状态。

还有一个缘由,componentWillMount是服务端渲染惟一会调用的生命周期函数,若是你在此方法中请求数据,那么服务端渲染的时候,在服务端和客户端都会分别请求两次相同的数据,这显然也咱们想看到的结果。

特别是有了 React Fiber,更有机会被调用屡次,故请求不该该放在componentWillMount中。

还有一个错误的使用是在componentWillMount中订阅事件,并在componentWillUnmount中取消掉相应的事件订阅。事实上只有调用componentDidMount后,React 才能保证稍后调用componentWillUnmount进行清理。并且服务端渲染时不会调用componentWillUnmount,可能致使内存泄露。

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

对于该函数,通常状况,若是项目有使用,则是一般把现有 componentWillMount 中的代码迁移至 componentDidMount 便可。

UNSAFE_componentWillUpdate

当组件收到新的 props 或 state 时,会在渲染以前调用 UNSAFE_componentWillUpdate()。使用此做为在更新发生以前执行准备更新的机会。初始渲染不会调用此方法。

注意,不能在该方法中调用 this.setState();在 componentWillUpdate 返回以前,你也不该该执行任何其余操做(例如,dispatch Redux 的 action)触发对 React 组件的更新。

首先跟上面两个函数同样,该函数也发生在 render 以前,也存在一次更新被调用屡次的可能,从这一点上看就依然不可取了。

其次,该方法常见的用法是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate 中进行相应的处理。但 React 16 版本后有 suspense、异步渲染机制等等,render 过程能够被分割成屡次完成,还能够被暂停甚至回溯,这致使 componentWillUpdatecomponentDidUpdate 执行先后可能会间隔很长时间,这致使 DOM 元素状态是不安全的,由于这时的值颇有可能已经失效了。并且足够使用户进行交互操做更改当前组件的状态,这样可能会致使难以追踪的 BUG。

为了解决这个问题,因而就有了新的生命周期函数:

getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate 在最近一次渲染输出(提交到 DOM 节点)以前调用。它使得组件能在发生更改以前从 DOM 中捕获一些信息(例如,滚动位置)。今生命周期的任何返回值将做为第三个参数传入 componentDidUpdate(prevProps, prevState, snapshot)

componentWillUpdate 不一样,getSnapshotBeforeUpdate 会在最终的 render 以前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是能够保证与 componentDidUpdate 中一致的。

虽然 getSnapshotBeforeUpdate 不是一个静态方法,但咱们也应该尽可能使用它去返回一个值。这个值会随后被传入到 componentDidUpdate 中,而后咱们就能够在 componentDidUpdate 中去更新组件的状态,而不是在 getSnapshotBeforeUpdate 中直接更新组件状态。避免了 componentWillUpdatecomponentDidUpdate 配合使用时将组件临时的状态数据存在组件实例上浪费内存,getSnapshotBeforeUpdate 返回的数据在 componentDidUpdate 中用完即被销毁,效率更高。

来看官方的一个例子:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 咱们是否在 list 中添加新的 items?
    // 捕获滚动位置以便咱们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 若是咱们 snapshot 有值,说明咱们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

若是项目中有用到componentWillUpdate的话,升级方案就是将现有的 componentWillUpdate 中的回调函数迁移至 componentDidUpdate。若是触发某些回调函数时须要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,而后在 componentDidUpdate 中统一触发回调或更新状态。

除了这些,React 16 版本的依然还有大改动,其中引人注目的就是 Fiber,以后我还会抽空写一篇关于 React Fiber 的文章,能够关注个人我的技术博文 Github 仓库,以为不错的话欢迎 star,给我一点鼓励继续写做吧~

参考:

相关文章
相关标签/搜索