发现好像有些没有过的生命周期函数,还没完全弄清楚...
一、组件的生命周期
组件的生命周期,主要分为 Mounting(挂载)、Updating(更新)、Unmounting(卸载)三个阶段。
Mounting
当组件示例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
以下生命周期方法即将过时,在新代码中应该避免使用它们:
UNSAFE_componentWillMount()
。
Updating
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
请注意,以下方法即将过时,在新代码中应该避免使用它们:
UNSAFE_componentWillUpdate()
、UNSAFE_componentWillReceiveProps()
Unmounting
当组件从 DOM 中移除时会调用如下方法:
- componentWillUnmount()
Error Handling(错误处理)
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
- static getDerivedStateFromError()
- componentDidCatch()
二、Mounting(挂载)
2.1 constructor
constructor(props)
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
在 React 组件挂载之前,会调用它的构造函数。在为 React.Component
子类实现构造函数时,应该在其他语句之前调用 super(props)
。否则,this.props
在构造函数中可能会出现未定义的 bug。
一般需重写构造函数,只做两件事:
- 初始化组件内部 state,即
this.state = { ... }
。 - 为时间处理函数绑定实例,如:
this.handleClick = this.handleClick.bind(this)
否则无需为 React 组件实现构造函数。
2.2 static getDerivedStateFromProps(不常用)
static getDerivedStateFromProps(props, state)
需要注意的是,此方法无法访问组件实例,即不能使用
this
。
getDerivedStateFromState
会在调用 render
方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个 对象
来更新 state,如果返回 null
则不更新任何内容。
与 componentWillReceiveProps
不同的是,getDerivedStateFromProps
不管原因是什么,都会在每次渲染前触发此方法。而 componentWillReceiveProps
仅在父组件重新渲染时触发,而不是在内部调用 setState
时。
请避免使用派生 state。
2.2.1 UNSAFE_componentWillReceiveProps
此生命周期之前名为
componentWillReceiveProps
。该名称将继续使用至 React 17。在 React 16.3 之后,它的替代者是getDerivedStateFromProps
。
UNSAFE_componentWillReceiveProps()
会在已挂载的组件接收新的 props
之前被调用。如果你需要更新状态以响应 prop
更新(例如,重置它),你可以比较 this.props
和 nextProps
并在此方法中使用 this.setState()
执行 state
转换。
请注意,如果父组件导致组件重新渲染,即使没有 props
没有更改,也会调用此方法。如果只是想处理更改,请确保进行当前值与变更值的比较。
在挂载过程,React 不会针对初始 props
调用 UNSAFE_componentWillReceiveProps()
。组件只会在组件的 props
更新时调用此方法。调用 this.setState()
通常不会触发 UNSAFE_componentWillReceiveProps()
。
2.3 UNSAFE_componentWillMount
该生命周期之前名为
componentWillMount
,旧名称仍可使用至 React 17.
UNSAFE_componentWillMount()
在 constructor
之后,render
之前被调用,因此在此方法中同步调用 setState()
不会触发额外渲染。通常,我们建议使用 constructor()
来初始化 state。
避免在此方法引入任何副作用或订阅,请放置在
componentDidMount()
。
此方法是服务端渲染唯一会调用的生命周期函数。
2.4 render
render()
方法是 class 组件中唯一必须实现的方法。
当 render
被调用,它会检测 this.props
和 this.state
的变化并返回以下类型之一:
- React 元素:通常通过 JSX 创建。例如,
会被 React 渲染为 DOM 节点,
会被 React 渲染为自定义组件,无论是还是
均为 React 元素。 - 数组或 fragments: 使得
render
方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。 - Portals:可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或
null
:什么都不渲染。(主要用于支持返回test &&
的模式,其中test
为布尔类型。)
render()
函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。
如需与浏览器进行交互,请在 componentDidMount()
或其他生命周期方法中执行你的操作。保存 render()
为纯函数,可以使组件更容易思考。
需要注意都是,
shouleComponentUpdate()
返回false
,则不会调用render()
。
2.5 componentDidMount
componentDidMount()
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
这个地方是比较合适添加订阅。如果添加了订阅,请不要忘记在 componentWillUnmount()
里取消订阅。
你可以在
componentDidMount()
里直接调用setState()
。它将会触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在constructor()
中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 或 tooltips 等情况下,你可以使用此方式处理。
三、Updating(更新)
3.1 shouldComponentUpdate(不常用)
shouldComponentUpdate(nextProps, nextState)
根据 shouldComponentUpdate()
的返回值,判断 React 组件的输出是否受当前 state
或 props
更改的影响。默认行为是 state
每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。
当
props
或state
发生变化时,shouldComponentUpdate()
会在渲染执行之前被调用。返回值默认为true
。首次渲染或使用forceUpdate()
时不会调用该方法。
此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()
。PureComponent 会对 props
和 state
进行浅层比较,并减少了跳过必要更新的可能性。
如果你一定要手写编写此函数,可以将 this.props
与 nextProps
以及 this.state
与 nextState
进行比较,并返回 false
以告知 React 可以跳过更新。请注意,返回 false
并不会阻止子组件在 state
更改时重新渲染。
不建议在 shouldComponentUpdate()
中进行深层比较或使用 JSON.stringify()
。这样非常影响效率,且会损害性能。
目前,如果 shouldComponentUpdate()
返回 false
,则不会调用 UNSAFE_componentWillUpdate()
、render()
、componentDidUpdate()
。后续版本,React 可能会将 shouldComponentUpdate
视为提示而不是严格的指令,并且当返回 false
时,仍可能导致组件重新渲染。
3.2 UNSAFE_componentWillUpdate(不常用)
UNSAFE_componentWillUpdate(nextProps, nextState)
此生命周期之前名为
componentWillUpdate
。该名称将继续使用至 React 17。
当组件收到新的 props
或 state
时,会在渲染之前调用 UNSAFE_componentWillUpdate()
。使用此作为更新发生之前执行准备更新的几乎。初始渲染不会调用此方法。
注意,你不能此方法中调用 setState()
;在 UNSAFE_componentWillUpdate()
返回之前,你不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新。
通常,此方法可以替换为 componentDidUpdate()
。如果你在此方法中读取 DOM 信息(例如,为了保存滚动违章),则可以将此逻辑移至 getSnapShotBeforeUpdate()
中。
如果
shouldComponentUpdate()
返回false
,则不会调用UNSAFE_componentWillUpdate()
。
3.3 componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate()
会在更新后被立即调用,但首次渲染不会执行此方法。
当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props
进行了比较,也可以选择在此处进行网络请求。(例如,当 props
为发生变化时,则不会执行网络请求。)
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props)
// 若直接 setState() 会导致死循环。
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID)
}
}
你也可以在 componentDidUpdate()
中直接调用 setState()
,但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props
“镜像”给 state,请考虑直接使用 props
。
如果组件实现了 getSnapshotBeforeUpdate()
生命周期(不常用),则它的返回值将作为 componentDidUpdate()
的第三个参数 snapshot
传递。否则此参数将为 undefined
。
需要注意的是,
shouldComponentUpdate()
返回值为false
,则不会调用componentDidUpdate()
。
四、Unmounting(卸载)
4.1 componentWillUnmount
componentWillUnmount()
会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如清除 timer,取消网络请求或清除 componentDidMount()
中创建的订阅等。
componentWillUnmount()
中不应调用 setState()
,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
需要注意的是,我们常说的组件销毁,只是组件从页面中删除而已。而并不意味着它真正被销毁,被 GC 回收。我们所写的 JSX 形式的组件,都会被 Babel 转化成
React.createElement()
形式的JS 对象(没错,React 组件本质上就是一个 JavaScript 对象)。因此只有它不再被引用,垃圾回收机制才会回收掉,这才算真正销毁。这也是为什么要做componentWillUnmount
清除副作用的原因。
五、其他
5.1 defaultProps
defaultProps
可以为 Class 组件添加默认 props
。这一般用于 props
未赋值,但又不能为 null
的情况。例如:
class CustomButton extends Component {
// ...
}
CustomButton.defaultProps = {
type: 'primary'
}
利用 ES6 的 class 静态语法
class CustomButton extends Component {
static defaultProps = {
type: 'primary'
}
// ...
}
5.2 一些问题
React 16 之后采用了 Fiber 架构,只有 componentDidMount
生命周期函数是确定被执行一次的,类似 ComponentWillMount
的生命周期钩子都有可能执行多次,所以不加以在这些生命周期中做有副作用的操作,比如请求数据之类。
3.3 componentDidCatch()
未完待续...
五、逐步迁移路径
React 遵循语义版本控制,因此这种变化将是逐步的。我们目前的计划是:
- 16.3:为不安全的生命周期引入别名,
UNSAFE_componentWillMount
、UNSAFE_componentWillReceiveProps
和UNSAFE_componentWillUpdate
。(旧的生命周期名称和新的别名都可以在此版本中使用。) - 未来 16.x 版本:为
componentWillMount
、componentWillReceiveProps
和componentWillUpdate
启用废弃告警。(旧的生命周期名称和新的别名都将在这个版本中工作,但是旧的名称在开发模式下会产生一个警告。) - 17.0:删除
componentWillMount
、componentWillReceiveProps
和componentWillUpdate
。(在此版本之后,只有新的UNSAFE_
生命周期名称可以使用。)
这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后。
六、示例
由于使用新 API(如
getDerivedStateFromProps
、getSnapshotBeforeUpdate
)时,React 不会调用组件的 “unsafe” 生命周期(如UNSAFE_componentWillMount
、UNSAFE_componentWillReceiveProps
、UNSAFE_componentWillUpdate
),因此暂时先注释掉新 API。同时使用,会出现类似如下警告⚠️:
Warning: Unsafe legacy lifecycles will not be called for components using new component APIs.
LifeCycle uses getDerivedStateFromProps() but also contains the following legacy lifecycles:
UNSAFE_componentWillMount
The above lifecycles should be removed. Learn more about this warning here:
https://fb.me/react-unsafe-component-lifecycles
示例基于 React 16.12.0 版本。
import React, { Component } from 'react'
export default class LifeCycle extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.setCount = this.setCount.bind(this)
console.log('---> constructor')
}
static getDerivedStateFromProps(prevProps, prevState) {
console.log('---> getDerivedStateFromProps')
return null
}
// 不会为使用新组件 API 的组件调用不安全的遗留生命周期。
// Warning: Unsafe legacy lifecycles will not be called for components using new component APIs.
// LifeCycle uses getDerivedStateFromProps() but also contains the following legacy lifecycles: UNSAFE_componentWillMount
// UNSAFE_componentWillMount() {
// console.log('---> UNSAFE_component')
// }
componentDidMount() {
console.log('---> componentDidMount')
}
setCount() {
this.setState({ count: this.state.count + 1 })
}
render() {
console.log('---> render')
return (
Count: {this.state.count}
)
}
}
当首次加载加载时,分别触发了以下生命周期,打印结果:
---> constructor
---> UNSAFE_componentWillMount
---> render
---> componentDidMount
当我们点击按钮通过 setState
更新 count
时,会触发以下动作:
---> shouldComponentUpdate
---> UNSAFE_componentWillUpdate
---> render
---> componentDidUpdate
未完待续...
七、参考
- React Lifecycle Methods diagram
- 你真的了解 React 生命周期吗
- 检测意外的副作用