likes
comments
collection
share

React 生命周期,了解组件的一生 !

作者站长头像
站长
· 阅读数 16

前言

当我们在学习一个新的框架或者技术时,了解组件的生命周期是非常重要。从被创建到更新以及被销毁的整个过程,它包含了许多不同的阶段。了解组件的生命周期可以帮助我们更好地理解组件的行为和工作原理。

我们通常利用生命周期来实现一些特定的逻辑,比如在组件挂载时初始化数据,或者在组件更新时更新状态...

同时也可以通过生命周期方法来优化组件的性能,避免不必要的渲染和计算。掌握组件的生命周期可以让我们更加高效地开发。

生命周期函数

React 中的生命周期可以分为三个阶段:

  • 挂载阶段:组件被创建并插入到 DOM 中
  • 更新阶段:组件的 props 或 state 发生变化时触发的组件更新
  • 卸载阶段:组件被从 DOM 中移除

在 React 16.3 版本引入了新的生命周期方法,也就是所谓的 new Lifecycle。在此之前,React 使用的是“旧版生命周期”(old Lifecycle)。

old lifecycle

React 生命周期,了解组件的一生 !new lifecycle

React 生命周期,了解组件的一生 !

  • 挂载
    • constructor
    • getDerivedStateFromProps
    • render
    • componentDidMount
  • 更新: setState、useState、props变化、forceUpdate 会触发组件的更新
    • getDerivedStateFromProps (新)
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate (新)
    • componentDidUpdate
  • 卸载
    • componentWillUnmount

从 v16.3 开始废弃了 componentWillMount、componentWillReceiveProps、componentWillUpdate 三个钩子,并为这几个钩子提供了别名

  • UNSAFE_componentWillMount:当组件被渲染出来之前
  • UNSAFE_componentWillReceiveProps:当组件收到新的props之前
  • UNSAFE_componentWillUpdate:组件更新前

React17 将只提供别名,以别名的方式就是来恶心你的,让你不要使用🤣。

废弃原因

Facebook花了两年多的时间搞出了React Fiber, 在v16之前的版本中,react的更新过程是同步的,react 在加载或者更新组件的过程是同步的,期间不允许其他的操作,往往一个主线程长时间被占用,会导致页面性能问题。

React Fiber 的机制: 把更新过程碎片化,利用浏览器 requestIdleCallback 将可中断的任务进行分片处理,维护每一个分片的数据结构,就是Fiber,每一个小片的运行时间很短,这样唯一的线程就不会被独占。

什么是 Fiber?

在计算机中,除了 进程(Process)和线程(Thread)的概念外还有一个名为 Fiber (纤维)的概念,意指比Thread更细的线,也就是比线程控制得更精密的并发处理机制。

Fiber 是一个执行单元也是一种数据结构,支撑 Fiber 构建任务的运转。

在渲染过程中,react 将渲染任务拆分成了一个个的小任务进行处理,每一个小任务指的就是 Fiber 节点的构建。 拆分的小任务会在浏览器的空闲时间被执行,每个任务单元执行完成后,React 都会检查是否还有空余时间,如果有就交换主线程的控制权。

Fiber 的总体思路就是利用循环和链表代替递归去优化性能,Fiber 架构有两个阶段,

  • render 阶段负责构架 Fiber 对象和链表,找出需要更新的 DOM
  • commit 阶段负责去构建 DOM

React Fiber对生命周期的影响

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来

React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。 第一阶段:构建 Fiber 对象,构建链表,在链表中标记要执行的 DOM 操作 ,这个阶段是可以被打断的

  • render: render 是纯函数能够被多次调用
  • componentWillMount
  • componentWillReceiveProps: 即使当前组件不更新,只要父组件更新也会引发这个函数被调用,允许被多次调用
  • shouldComponentUpdate:这函数的作用就是返回一个true或者false,不会产生任何副作用,多调几次也没关系
  • componentWillUpdate

第二阶段:根据构建好的链表DOM 的,更新DOM的过程中不会被打断

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

在React Fiber中,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用! React Fiber Reconciliation 这个过程有可能暂停然后继续执行,所以挂载和更新之前的生命周期钩子就有可能不执行或者多次执行。

挂载阶段

constructor

只会在组件初始化的时候触发一次

React构造函数仅用于以下情况:

  • 设置初始化状态函数内部 state:因为组件的生命周期中任何函数都可能要访问 State,在生命周期中第一个被调用的构造函数便是初始化 state 最理想的地方。
  • 绑定成员函数上下文引用:
    • 因为在 ES6 语法下,类的每个成员函数在执行时的 this 并不是和类实例自动绑定的;而在构造函数中 this 就是当前组件实例,所以在构造函数中将 this 为当前类实例;
    • 建议定义函数方法时直接使用箭头函数,就无须在构造函数中进行函数的 bind 操作。
  • 为事件处理函数 绑定实例
class Sample extends React.Component {
  constructor(props, context, updater) {
    super(props);
    this.state = {
      foo: 'InitailValue',
    };
  }
}

Sample.defaultProps = {
  bar: 'InitialValue',
};
  • props:继承 React.Component 的属性方法,它是不可变的 read-only
  • context:全局上下文。
  • updater:包含一些更新方法的对象
    • this.setState 最终调用的是 this.updater.enqueueSetState
    • this.forceUpdate 最终调用的是 this.updater.enqueueForceUpdate 方法,所以这些 API 更多是 React 调用使用,暴露出来以备开发者不时之需。

不能在 constructor()构造函数内部调用 this.setState(), 因为此时第一次 render()还未执行,也就意味DOM节点还未挂载

static getDerivedStateFromProps

该函数会在组件化实例化后和重新渲染前调用(生成 VirtualDOM 之后,实际 DOM 挂载之前)

无论是父组件的更新、props 的变化或通过 setState 更新组件内部的 State,它都会被调用。

返回值:该生命周期函数必须有返回值,返回一个对象来更新 State,若返回 null 表明新 props 不更新 state。

新特性:当组件实例化时,该方法替代了 componentWillMount,而当接收新的 props 时,该方法替代了 componentWillReceiveProps 和 componentWillUpdate。

适用场景:表单获取默认值

static getDerivedStateFromProps(nextProps, prevState) {
  if (nextProps.translateX !== prevState.translateX) {
    return {
      translateX: nextProps.translateX
    }
  }
  if (nextProps.data !== prevState.data){
    return {
      data: nextProps.data
    }
  }
  return null;
}

**注意: **

  • getDerivedStateFromProps 是一个静态函数,不能使用this, 也就是只能作一些无副作用的操作
  • 如果父组件导致了组件的重新渲染,即使属性没有更新,这一方法也会被触发;

该生命周期函数被设计成静态方法的目的是为了保持该方法的纯粹。能够起到限制开发者无法访问 this 也就是实例的作用,这样就不能在里面调用实例方法或者 setState 以破坏该生命周期函数的功能。

通过更具父组件输入的 props 按需更新 state,这种 state 叫做衍生 state,返回的对象就是要增量更新的 state,除此之外不应该在里面执行任何操作。

UNSAFE_componentWillMount

预装载函数 componentWillMount 将在 React v17 正式废弃,用 UNSAFE_componentWillMount 别名代替。

触发时机:在构造函数和装载组件(将 DOM 树渲染到浏览器中)之间触发。装载组件后将执行 render 渲染函数。因此在此生命周期函数里使用 setState 同步设置组件内部状态 state 将不会触发重新渲染。

注意:避免在该方法中引入任何的副作用(Effects)或订阅(Subscription)。对于这些使用场景,建议提前到构造函数中。

render

第一次以及之后组件发生更新,都会执行render,一个仅仅用于渲染的纯函数,返回值完全取决于 state 和 props,而生成虚拟 DOM 树进行对比是在 render 函数执行了之后进行

class组件中唯一必须实现的方法,用于渲染 DOM, render()方法必须有返回值,返回 null 或 false 表示不渲染任何 DOM 元素。

一个仅仅用于渲染的**纯函数,**此渲染函数并不做实际的渲染动作(渲染到 DOM 树),它返回一个 JSX 的描述对象(及组件实例),何时进行真正的渲染是有 React 库决定。

函数当且仅当下列两种情况才会被调用:

  • 组件初始化
  • 组件的 props 或 state 发生变化

在纯函数中

  • 请勿在此函数中使用 setState 方法;
  • 请勿在此函数中修改 props、state 以及数据请求等具有副作用的操作。

componentDidMount

在组件挂载完成之后 (插入DOM树后) 立即调用

触发时机:组件完全挂载到网页上后触发 使用场景:

  • 发送网络请求;
  • 任何依赖于 DOM 的初始化操作;
  • 添加事件监听;如果使用了 Redux 之类的数据管理工具,也能触发 action 处理数据变化逻辑。
  • 此钩子函数中允许使用 setState 改变组件内 State。

注意:该生命周期函数在进行服务器端渲染时不会触发(仅客户端有效)。

更新阶段:

属性(Props)或状态(State)的改变会触发一次组件的更新,但是组件是否会重新渲染,这取决于 shouldComponentUpdate。

shouldComponentUpdate

在组件更新之前调用,可以控制组件是否进行更新, 返回true时组件更新, 返回false则不更新 shouldComponentUpdate(nextProps, nextState)

当props或state发生变化,在重新渲染执行之前,第一个是即将更新的 props 值,第二个是即将跟新后的 state 值,可以根据更新前后的 props 或 state 来比较加一些限制条件,决定是否更新,进行性能优化

当该方法返回的布尔值 false 告知 React 无须重新渲染时,render、UNSAFE_componentWillUpdate 和 componentDidUpdate 等生命周期钩子函数都不会被触发。

  • 此钩子函数在初始化渲染和使用了 forceUpdate 方法的情况下不会被触发,使用 forceUpdate 会强制更新
  • 请勿在此函数中使用 setState 方法,会导致循环调用。

getSnapshotBeforeUpdate

**保存状态快照。**该生命周期函数会在组件即将挂载时触发,它的触发在 render 渲染函数之后。

由此可见,render 函数并没有完成挂载操作,而是进行构建抽象 UI(也就是 Virtual DOM)的工作。该生命周期函数执行完毕后就会立即触发 componentDidUpdate 生命周期钩子。

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

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

该生命周期函数能让你捕获某些从 DOM 中才能获取的(可能会变更的例如滚动位置),此生命周期返回的任何值都会作为第三个参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null。

componentDidUpdate

组件每次重新渲染后会触发 componentDidUpdate ; 但是只有在组件首次渲染(即初始化)时触发的是 componentDidMount 钩子函数,初始化渲染后componentDidMount 不会再被触发。

适用场景:操作 DOM;发送网络请求。

在 componentDidUpdate 生命周期函数中调用 setState 方法时,确实需要加上条件判断,以避免死循环的发生。

通过判断 prevProps、prevState 和 this.state 之间的数据变化,来判断是否执行相关的 state 变更逻辑,这使得尽管在 componentDidUpdate 中调用了 setState 进行再更新,如果条件不成立state就不会进行更新,从而避免了死循环的发生。

componentDidUpdate(prevProps){
    if (this.props.id !== prevProps.id){
        this.fetchData(this.props.id);
    }
}

可以进行前后props的比较进行条件语句的限制,来进行 setState() , 否则会导致死循环。

相比装载阶段的生命周期函数,更新阶段的生命周期函数使用的相对来说要少一些。常用的是 getDerivedStateFromProps、shouldComponentUpdate, 前者经常用于根据新 props 的数据去设置组件的 State,而后者则是常用于优化,避免不必要的渲染。

卸载阶段

componentWillUnmount

在组件卸载和销毁之前触发。可以利用这个生命周期方法进行事件的解绑

用于移除事件监听器;取消网络请求;取消定时器;解绑 DOM 事件。 在该方法中调用 setState 不会触发 render,因为所有的更新队列,更新状态都被重置为 null。

父子组件加载顺序

1. 父子组件初始化

父子组件初次加载会在子组件挂挂载完成后继续父组件的挂载

  • Parent 组件: constructor()
  • Parent 组件: getDerivedStateFromProps()
  • Parent 组件: render()
  • Child 组件: constructor()
  • Child 组件: getDerivedStateFromProps()
  • Child 组件: render()
  • Child 组件: componentDidMount()
  • Parent 组件: componentDidMount()

当执行render子组件的时候,才会进入子组件的生命周期,子组件的周期结束后,再回到上级的周期。

2. 改变子组件自身状态

  • Child 组件: getDerivedStateFromProps()
  • Child 组件: shouldComponentUpdate()
  • Child 组件: render()
  • Child 组件: getSnapshotBeforeUpdate()
  • Child 组件: componentDidUpdate()

3.修改父组件传入给子组件的 props 当父组件修改传递的 props 值时,会触发父子组件的更新

  • Parent 组件: getDerivedStateFromProps()
  • Parent 组件: shouldComponentUpdate()
  • Parent 组件: render()
  • Child 组件: getDerivedStateFromProps()
  • Child 组件: shouldComponentUpdate()
  • Child 组件: render()
  • Child 组件: getSnapshotBeforeUpdate()
  • Parent 组件: getSnapshotBeforeUpdate()
  • Child 组件: componentDidUpdate()
  • Parent 组件: componentDidUpdate()

  1. 只修改父组件的值 当父组件的值被修改时,会触发父组件的 render() 方法重新渲染,并递归地 r-render 子组件。这个过程中,会依次触发所有子组件的生命周期方法:
  • Parent 组件: getDerivedStateFromProps()
  • Parent 组件: shouldComponentUpdate()
  • Parent 组件: render()
  • Child 组件: getDerivedStateFromProps()
  • Child 组件: shouldComponentUpdate()
  • Child 组件: render()
  • Child 组件: getSnapshotBeforeUpdate()
  • Parent 组件: getSnapshotBeforeUpdate()
  • Child 组件: componentDidUpdate()
  • Parent 组件: componentDidUpdate()

所以不管父组件有没有把数据传递给子组件,只要父组件 setState,都会走一遍子组件的更新周期。 而且子组件被动更新会比主动更新所执行的流程多出来一个 componentWillReceiveProps 方法。

5.子组件修改自身state

当我们修改子组件中的数据时,只有子组件会经历更新生命周期,而父组件不会。

  • 子组件 getDerivedStateFromProps
  • 子组件 shouldComponentUpdate
  • 子组件 render
  • 子组件 getSnapShotBeforeUpdate
  • 子组件 componentDidUpdate

6. 卸载子组件

触发父组件的重新渲染、子组件销毁

  • Parent 组件: getDerivedStateFromProps()
  • Parent 组件: shouldComponentUpdate()
  • Parent 组件: render()
  • Parent 组件: getSnapshotBeforeUpdate()
  • Child 组件: componentWillUnmount()
  • Parent 组件: componentDidUpdate()

小结:

  • 当子组件自身状态改变时,不会对父组件产生副作用的情况下,父组件不会进行更新,即不会触发父组件的生命周期
  • 当父组件中状态发生变化(包括子组件的挂载以及卸载)时,会触发自身对应的生命周期以及子组件的更新

总结

了解 react 的生命周期可以帮助我们更好地理解 react 中的运作原理, react 中的 new lifecycle 提供了更加灵活和可控的方式来管理组件的状态和行为,同时也更加安全和可靠。但是需要注意的是,由于名称和调用时机的改变,可能会对一些已有代码造成影响,需要进行相应的调整。

参考

  1. 详解react生命周期和在父子组件中的执行顺序