React 生命周期

React 生命周期

React 在 16.0 的版本当中,针对其之前的生命周期钩子进行了一定层度上的调整,所以在本章当中我们就来简单的汇总一下两个版本的生命周期有什么区别,以及为什么要进行这样的调整,更多关于 React 生命周期相关内容可以参考 官方文档

React 16.0 之前的生命周期

其实简单来说,React 16.0 之前的生命周期可以分为四个阶段

一、组件初始化阶段(Initialization)

  • constructor()

主要用来做一些组件的初始化工作,如定义 this.state 的初始内容,如下示例,Test 类继承了 React Component 这个基类,也是因为继承了这个基类,才能拥有 render(),生命周期等方法可以使用,这也说明了为什么函数组件不能使用这些方法的原因

super(props) 用来调用基类的构造方法 constructor(),也将父组件的 props 注入给子组件,供子组件读取,组件中的 props 只读不可变,state 可变,也可以根据 props 来设置 state

1
2
3
4
5
6
7
8
class Test extends Comonent {
constructor(props) {
super(props)
this.state = {
counter: props.initialCounterValue,
}
}
}

当然,使用构造函数的方式是可选的,如果 Babel 设置了支持 类字段,则可以像下面这样初始化 state

1
2
3
4
5
class Test extends Component {
state = {
counter: 0
}
}

这种方法使用较为常见,我们仍然可以根据 props 设置 state

1
2
3
4
5
class Test extends Component {
state = {
counter: this.props.initialCounterValue,
}
}

但是,如果需要使用 ref,可能仍需要构造函数

1
2
3
4
5
6
7
8
9
class Test extends Component {
constructor(props) {
super(props)
this.state = {
counter: props.initialCounterValue,
}
this.myRef = React.createRef()
}
}

我们需要构造函数调用 createRef 来创建对元素的引用,以便我们可以将它传递给某个组件,另外还可以在构造函数当中进行函数绑定,这也是可选的

二、组件的挂载阶段(Mounting)

此阶段分为 componentWillMount(),render(),componentDidMount() 三个时期

  • componentWillMount()
    • 在组件挂载到 DOM 前调用,且只会被调用一次,在这个生命周期函数中调用 this.setState() 不会引起组件重新渲染,也可以把写在这里面的内容提前到 constructor() 中,所以项目中很少使用,所以这个生命周期钩子将『被废弃』
  • render()
    • 根据组件的 props 和 state 变化来执行渲染工作,render 是纯函数,所谓的纯函数指的是函数的返回结果只依赖于它的参数,函数执行的过程中没有副作用产生
  • componentDidMount()
    • 组件挂载到 DOM 后调用,且只会调用一次,一般数据请求都会放到这个钩子当中来进行执行

三、组件的更新阶段(Updation)

此阶段分为以下几个流程,这个阶段涉及到的内容较多,所以我们来稍微深入的了解一些

  • UNSAFE_componentWillReceiveProps()(即将过时)
  • shouldComponentUpdate()
  • UNSAFE_componentWillUpdate()(即将过时)
  • render()
  • componentDidUpdate()

首先要明确 React 组件更新机制,setState 引起的 state 更新或者父组件重新 render 引起的 props 更新,更新后的 state 和 props 相对之前无论是否有变化,都将引起子组件的重新 render

造成组件更新主要有两类情况,我们分类来进行阐述,其中『第一大类』是父组件重新 render,它引起子组件重新 render 的情况又有两种,第一种情况就是直接使用父组件传递进来的 props

这种方式,父组件改变 props 后,子组件重新渲染,由于直接使用的 props,所以我们不需要做什么就可以正常显示最新的 props,每当父组件重新 render 导致的重新传递 props,子组件将直接跟着重新渲染,无论 props 是否有变化,这种方式还可以通过 shouldComponentUpdate 方法优化

1
2
3
4
5
6
7
8
9
10
11
class Child extends Component {
// 应该使用这个方法,否则无论 props 是否有变化都会导致组件跟着重新渲染
shouldComponentUpdate(nextProps) {
if (nextProps.someThings === this.props.someThings) {
return false
}
}
render() {
return <div>{this.props.someThings}</div>
}
}

第二种则是在 componentWillReceiveProps 方法中,将 props 转换成自己的 state,这种方式,我们使用的是 state,所以每当父组件每次重新传递 props 时,我们需要重新处理下,将 props 转换成自己的 state,这里就用到了 componentWillReceiveProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Child extends Component {
constructor(props) {
super(props)
this.state = {
someThings: props.someThings
}
}

// 父组件重传 props 时候就会调用这个方法
componentWillReceiveProps(nextProps) {
// 将父组件传入进来的 props 赋值给 state
this.setState({
someThings: nextProps.someThings
})
}

render() {
return <div>{this.state.someThings}</div>
}
}

根据官方描述『在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染』,这是因为 componentWillReceiveProps 中判断 props 是否变化了,若变化了则 this.setState() 将引起 state 的变化,从而引起 render,此时就没有必要再做第二次因重传 props 引起的 render 了,不然重复做一样的渲染

『第二大类』是组件本身调用 setState 无论 state 有没有变化,都会引起重新渲染,这种情况可以通过 shouldComponentUpdate 方法优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Child extends Component {
constructor(props) {
super(props)

this.state = {
someThings: 1
}
}

// 应该使用这个方法,否则无论 state 是否有变化都将会引起组件的重新渲染
shouldComponentUpdate(nextStates) {
// 如果更新后的 state 和当前的 state 对比没有变化,阻止重新渲染
if (nextStates.someThings === this.state.someThings) {
return false
}
}

// 虽然调用了setState,但是state并没有变化
handleClick = () => {
const preSomeThings = this.state.someThings
this.setState({
someThings: preSomeThings
})
}

render() {
return <div onClick={this.handleClick}>{this.state.someThings}</div>
}
}

弄清楚了 React 组件的更新机制,我们回归正题,来详细看看之前提到过的各个更新阶段

  • UNSAFE_componentWillReceiveProps()(nextProps)
    • 此方法只调用于 props 引起的组件更新过程中,响应 props 变化之后进行更新的唯一方式,参数 nextProps 是父组件传递给当前组件的新的 props
    • 但是父组件 render 方法的调用不能保证重传给当前组件 props 是有变化的,所以在此方法中根据 nextProps 和 this.props 来查明重传的 props 是否有改变,以及如果改变了要执行什么操作,比如根据新的 props 调用 this.setState 来触发当前组件的重新 render
  • shouldComponentUpdate(nextProps, nextState)
    • 此方法通过比较 nextProps,nextState 以及当前组件的 this.props,this.state,如果返回 true 时当前组件将继续执行更新操作,返回 false 则当前组件更新停止,借助此特性来减少组件的不必要渲染,优化组件的性能
    • 这里也可以看出,就算 componentWillReceiveProps() 中执行了 this.setState 更新了 state,但是在 render 之前(比如 shouldshouldComponentUpdate 和 componentWillUpdate)this.state 依然指向更新前的 state,不然 nextState 以及当前组件的 this.state 的对比就一直是 true 了
    • 如果 shouldComponentUpdate 返回 false 那就一定不用重新渲染(rerender)这个组件了,组件当中的组件元素(React Elements)也不用去对比,但是如果 shouldComponentUpdate 返回true 会进行组件的组件元素(React Elements)对比,如果相同,则不用重新渲染(rerender)这个组件,如果不同,会调用 render 函数进行重新渲染(rerender)
  • UNSAFE_componentWillUpdate()
    • 此方法在调用 render 方法前执行,在这边可以执行一些组件的更新发生前的工作,一般比较少用
  • render
    • render 方法触发组件的重新渲染
  • componentDidUpdate(prevProps, preState)
    • 此方法在组件更新后被调用,可以操作更新的 DOM,prevProps 和 preState 这两个参数指向组件更新前的 props 和 state

四、组件的卸载阶段(Unmounting)

  • componentWillUnmount
    • 此阶段只有一个生命周期方法 componentWillUnmount,此方法在组价被卸载时候调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除 componentDidMount 中手动创建的 DOM 元素等等,避免内存泄露

React 16.0 之后的生命周期

变更缘由

原来的生命周期在 React 16 推出的 Fiber 之后就不合适了(关于 React Fiber 相关内容可以见 深入 React Fiber),因为如果要开启 async rendering,那么在 render 函数之前的所有函数,都有可能执行多次,也就是

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

如果开启了 async rendering 而且又在以上这些生命周期方法当中去使用 Ajax 请求的话,那么 Ajax 将被无谓的多次调用,这明显不是我们期望的结果,而且在 componentWillMount 里面发起请求不管多快得到结果也赶不上首次 render

除了 shouldComponentUpdate 以外,其他在 render 函数之前的几个函数(componentWillMount/componentWillReceiveProps/componentWillUpdate)都将被 getDerivedStateFromProps 替代,也就是说,使用一个静态函数 getDerivedStateFromProps 来取代即将被废弃的这几个生命周期函数,就是建议或者说是强制我们在 render 之前只做无副作用的操作

React 16 刚推出的时候增加了一个 componentDidCatch 生命周期函数,这只是一个增量式的修改,完全不影响原有的生命周期函数,但是到了 React 16.3 版本推出了大改动,引入了两个新的生命周期函数 getDerivedStateFromProps 和 getSnapshotBeforeUpdate

下面我们就来结合上面已经介绍过的一些生命周期,再加上新增的两个一起来梳理一下在变更之后的先后流程,更为详细的流程可以参考官方文档或是 React 生命周期 这个网站来进行查看

  • 挂载阶段
    • constructor()
      • 构造函数,最先被执行,一般会在构造函数里初始化 state 对象或者给自定义方法绑定 this,也是唯一可以直接修改 state 的地方
    • static getDerivedStateFromProps(nextProps, prevState)
      • 每次 render() 都会调用,通常当 state 需要从 props 初始化时使用,不过建议尽量不要使用,因为维护两者状态一致性会增加复杂度
      • 典型的应用场景是表单控件获取默认值
    • render()
      • 纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的 DOM、React 组件、Fragment、Portals、字符串、数字、布尔和 null 等内容
    • componentDidMount()
      • 组件挂载后(插入 DOM 树中)立即调用,并且只会执行一次,此时我们可以获取到 DOM 节点并操作
      • 典型的应用场景是获取(订阅)外部资源,但是记得在卸载阶段中取消订阅
  • 更新阶段
    • static getDerivedStateFromProps()
      • 此方法在更新个挂载阶段都可能会调用
    • shouldComponentUpdate(nextProps, nextState)
      • 有两个参数 nextProps 和 nextState,表示新的属性和变化之后的 state,返回一个布尔值,true 表示会触发重新渲染,false 表示不会触发重新渲染,默认返回 true
      • 一般可以由 PureComponent 自动实现,通常利用此生命周期来优化 React 程序性能
    • render()
      • 更新阶段也会触发此生命周期
    • getSnapshotBeforeUpdate(prevProps, prevState)
      • 这个方法在最近一次 render() 之前调用,一般不太常用,利用它可以获取 render() 之前的 DOM 状态(比如滚动位置等)
      • 有两个参数 prevProps 和 prevState,表示之前的属性和之前的 state,一个返回值,会作为第三个参数传给 componentDidUpdate(),如果你不想要返回值,可以返回 null
      • 通常与 componentDidUpdate() 搭配使用(但是需要注意 getSnapshotBeforeUpdate() 和 componentDidUpdate() 之间可能存在延迟)
    • componentDidUpdate(prevProps, prevState, snapshot)
      • 该方法会在每次 UI 更新后会被立即调用(首次渲染不会执行此方法),有三个参数 prevProps,prevState,snapshot,表示之前的 props,state 和 snapshot,可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里
      • 如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()
      • 典型的应用场景是页面内容需要根据 props 变化重新获取数据
  • 卸载阶段
    • componentWillUnmount()
      • 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的 DOM 元素等垃圾清理工作
  • 错误处理
    • static getDerivedStateFromError()
      • 渲染备用 UI
    • componentDidCatch()
      • 主要用来打印错误信息,但是有一点需要注意,该方法仅适用于 渲染/生命周期 函数中的错误,如果应用程序在点击事件中抛出错误,它不会被捕获

下面我们主要来看看新增的两个方法

static getDerivedStateFromProps()

1
static getDerivedStateFromProps(props, state)

在 React 16.4 版本中,getDerivedStateFromProps() 方法无论是挂载(mounting)还是更新(updating),又或是其他什么引起的更新,全部都会被调用

这个生命周期就是为了替代 componentWillReceiveProps 存在的,所以在你需要使用 componentWillReceiveProps 的时候,就可以考虑使用 getDerivedStateFromProps 来进行替代了,两者的参数是不相同的,而 getDerivedStateFromProps 是一个静态函数,也就是这个函数不能通过 this 访问到 Class 的属性,也并不推荐直接访问属性,而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state

在 React 16.4 之后,getDerivedStateFromProps(nextProps, prevState) 方法会在组件创建和更新时的 render 方法之前被调用,值得注意的是,如果 props 传入的内容不影响你的 state,那么你就返回一个 null,这个返回值是必须的,所以尽量写在函数末尾

这里有一个需要注意的地方,即 getDerivedStateFromProps 前面要添加 static 保留字,声明为静态方法,否则会被 React 忽略掉,getDerivedStateFromProps 中的 this 指向为 undefined,因为静态方法只能被构造函数调用,而不能被实例调用

1
2
3
4
5
6
7
8
9
10
11
static getDerivedStateFromProps(nextProps, prevState) {
const { type } = nextProps
// 当传入的 type 发生变化的时候,更新 state
if (type !== prevState.type) {
return {
type,
}
}
// 否则,对于 state 不进行任何操作
return null
}

其实简单来说,getDerivedStateFromProps 的作用就是为了让 props 能更新到组件内部 state 中,它的可能使用场景大概有两个

第一个,无条件的根据 props 来更新内部 state,也就是只要有传入 prop 值,就更新 state

我们来看下面这个例子,假设我们有个一个表格组件,它会根据传入的列表数据来更新视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Table extends React.Component {
state = {
list: []
}

static getDerivedStateFromProps(props, state) {
return {
list: props.list
}
}

render() {
// 展示 list
// ...
}
}

上面的例子就是第一种使用场景,但是无条件从 prop 中更新 state 我们完全没必要使用这个生命周期,直接对 prop 值进行操作就好了,无需用 state 进行一个值的映射

第二个,只有 prop 值和 state 值不同时才更新 state 值

再看一个例子,这个例子是一个颜色选择器,这个组件能选择相应的颜色并显示,同时它能根据传入 prop 值显示颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class ColorPicker extends React.Component {
state = {
color: '#000000'
}

static getDerivedStateFromProps(props, state) {
if (props.color !== state.color) {
return {
color: props.color
}
}
return null
}

// 选择颜色方法
render() {
// 显示颜色和选择颜色操作
// ...
}
}

现在我们可以使用这个颜色选择器来选择颜色,同时我们能传入一个颜色值并显示,但是这个组件存在一些问题,比如如果我们传入一个颜色值后,再使用组件内部的选择颜色方法,我们会发现颜色不会变化,一直是传入的颜色值

这也是使用这个生命周期的一个比较常见的问题,为什么会出现这样的问题呢?我们在之前提到过,在 React 16.4^ 的版本中,setState 和 forceUpdate 也会触发这个生命周期,所以内部 state 变化后,又会走 getDerivedStateFromProps 方法,并把 state 值更新为传入的 prop,所以下面我们来稍微的调整一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class ColorPicker extends React.Component {
state = {
color: '#000000',
prevPropColor: ''
}

static getDerivedStateFromProps(props, state) {
if (props.color !== state.prevPropColor) {
return {
color: props.color
prevPropColor: props.color
}
}
return null
}

// 选择颜色方法
render() {
// 显示颜色和选择颜色操作
// ...
}
}

getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate() 被调用于 render 之后,可以读取但无法使用 DOM 的时候,它可以让我们的组件在可能更改之前从 DOM 捕获一些信息(例如滚动位置),此生命周期返回的任何值都将作为参数传递给 componentDidUpdate,但是一般使用不是很多,了解即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ScrollingList extends React.Component {
constructor(props) {
super(props)
this.listRef = React.createRef()
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否要添加新的 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 推出视图
// snapshot 是 getSnapshotBeforeUpdate 方法的返回值
if (snapshot !== null) {
const list = this.listRef.current
list.scrollTop = list.scrollHeight - snapshot
}
}

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

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×