老前言了

这篇文章的主要目的是记录,因为我还没到总结的时候。。文中引用的部分都来自于知乎的LeviDing大佬,瑞思拜

Hooks突突突地起飞了,理想中应该是不用学类组件中的生命周期了。

然而现实肯定和理想中的不一样,不然它叫个芽儿的现实。。。那么,生活总要面对,静下心来想想,除了维护以前未重构的业务,类组件还能为我们带来什么?

首先!面向对象编程全套的软件开发流程诶,搭配面向对象编程的各种项目精细化手段和最佳实践诶。

个人理解,由于函数式编程的局限(暂时?),可能需要很长一段时间,这种编程范式才能传播开,才能在更广的范围内(不仅于前端,如项目管理,开发流程,项目精细化等等)发展并经受住考验。

其次!通过类组件的生命周期,我们能切实的参与进React的渲染机制,再结合React的发展,以多种角度,以不同的高度,利用React进行开发(这就是自我发电式学习吗,那还真是有够好笑的呢( ๑乛◡乛๑))。

接下来!接下来啊,接下来没有了(阿这,我想不到了,我好菜T-T)。

仨阶段(仨阶段有5个小标题不是常识吗

我先扯个题外话(嗯,我扯题外话一直可以的┓(´_`)┏)。

类组件中,生命周期出现的意义是什么?(为开发人员提供的,参与进React渲染流程,并能进行干涉的,一种手段???待研究)Hooks没有生命周期,难道就没有办法做到类组件能做的事了吗?(待研究

好!没研究就先回来。

生命周期总览

看博客也就图一乐,真要学生命周期害得看图。

detail

这是原汁原味的图,按照官方的划分,我们从三个阶段依次来理解。

在其中,我们会讲到废弃的生命周期,因为这不仅能让我们了解React生命周期的发展,还能让我们更好的理解React的渲染流程(在这看渲染流程也就图一乐,真想了解透彻害得看Fiber啥的,不过结合起来也不错哦|ू・ω・` ))。

detail_modified

挂载阶段

挂载阶段,就是将组件挂载到DOM树上,需要经过如下:

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

几个过程,其中括号裹住的并不是真实的生命周期,可以理解为衔接两个生命周期的中间过程。(未使用有序列表是因为暂时还未确认实际上的调用顺序是否是这样的,不过至少设计上是这样的。

此处需要引用大佬的见解:

constructor

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

在构造函数中,我们一般会做两件事:

  1. 初始化 state

  2. 对自定义方法进行 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生命周期在两个阶段都会被调用。

  • 挂载阶段的constructor之后;

  • 更新阶段的props变化之后,通过setState更改状态之后,使用forceUpdate更新之后。

在挂载阶段,个人不太清楚它的应用场景。它既然设计成,并且官方也推荐其,仅用来处理单一更新来源的,随着props更改而更改的状态,就不该在挂载阶段被触发(感觉这个生命周期是需求——根据props更新state,与实际——实现异步渲染以提高应用的交互性,所折中的一种产物,理解有误还望大佬提点);

更新阶段,就,就请先往下康。。

新的 getDerivedStateFromProps 实际上与 componentDidUpdate 一起取代了以前的 UNSAFE_componentWillReceiveProps 函数。UNSAFE_componentWillReceiveProps 也是考虑到因为父组件引发渲染可能要根据 props 更新 state 的需要而设立的。

这里的UNSAFE_componentWillReceiveProps即是ReactV16.4之后被废弃,并在React V17出来之后将不再支持的生命周期。

对于这段文字,个人认为,componentDidUpdatecomponentDidMount是起相同的作用的,前者只是多了用来防止无限循环的参数和对另一个生命周期,getSnapshotBeforeUpdate的支持而已,有误望指出

UNSAVE_componentWillMount

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

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

正如大佬所说,在同步渲染中,UNSAVE_componentWillMount中处理各种是没有问题的,但由于框架的发展,需要优化渲染机制,所以以前的这个生命周期函数无法按照期望运行(此处可以去了解一下React的更新,Fiber的引入等等——指我自己,因为我还没怎么看 _| ̄|●)。

继续:

render

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

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

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

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

这里大佬应该是忘记打引号了,实际上应该都是先,生成虚拟DOM。。不过意思到了就行,个人认为第一个类型改为”React元素“会更好,小声bb

componentDidMount

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

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

请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modalstooltips 等情况下,你可以使用此方式处理

这里第二段中,“它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前”,不是很能理解到(意思是在componentDidMount调用setState(),会跳过Reconciliation的过程吗,不会被操作中断,直到第二次渲染结束?还是怎么个过程呢。。。),插眼。

更新阶段

更新即组件需要重新渲染的阶段,触发的原因可能有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 更改时重新渲染。

这里指的是父组件中的shouldComponentUpdate并不会递归地影响子组件的渲染。

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

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

这应该也是官方推荐使用内置实现的PureComponent的理由,当shouldComponentUpdate逐渐成为一种决定是否渲染的权重时,可能会有新的变动。

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

componentWillUnmount() 会在组件卸载及销毁之前直接调用。我们可以在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。注意不要在这个函数里调用 setState(),因为组件不会重新渲染了。

这里大佬附上了两个不常用的生命周期,有空也可以过一遍。

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

static getDerivedStateFromError()

static getDerivedStateFromError(error) {
  //...
}

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

componentDidCatch()

componentDidCatch(error, info) {
  //...
}

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

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

componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。它应该用于记录错误之类的情况:

如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。可以使用静态 getDerivedStateFromError() 来处理降级渲染。

废弃总结

上面介绍了一共三个废弃的旧生命周期,即有UNSAFE_前缀的

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

废弃的最关键原因,应该是:

在 React V16.0 之前,React 是同步渲染的,而在 V16.0 之后 React 更新了其渲染机制,是通过异步的方式进行渲染的,在 render 函数之前的所有函数都有可能被执行多次。

就这就这就这?

感觉哪里不对,奥,这只是生命周期啊,那没事了。 (:ι」∠)


你喜欢独处,却又担心寂寞,于是你爱上一阵又一阵迎面吹来的风