所谓的React生命周期,就是指组件从被创建出来,到被使用,最后被销毁的这么一个过程;
而在这个过程中,React提供了我们会自动执行的不同的钩子函数,我们称之为生命周期函数;
**组件的生命周期大致分为三个阶段:组件挂载阶段,组件更新阶段,组件销毁卸载阶段 **
16.4之后的声明周期图谱如下:
组件初始化阶段
由ReactDOM.render()触发—初次渲染
- constructor()
- getDerivedStateFromProps
- render()
- componentDidMount()
组件更新阶段
由组件内部this.setState()或者父组件重新render触发或者forceUpdate()
- getDerivedStateFromProps
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
组件卸载阶段
由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
React 15 生命周期和 React 16.3 生命周期在挂载阶段的主要差异在于,废弃了 componentWillMount,新增了 getDerivedStateFromProps。废弃了 componentWillUpdate() 新增了 getSnapshotBeforeUpdate
初始化阶段
由ReactDOM.render()触发—初次渲染
1.constructor()
2.componentWillMount()
3.render()
4.componentDidMount()
更新阶段
由组件内部this.setSate()或父组件重新render触发
1.shouldComponentUpdate()
2.componentWillUpdate()
3.render()
4.componentDidUpdate()
卸载组件
由ReactDOM.unmountComponentAtNode()触发
1.componentWillUnmount()
下面展开来说说组件的生命周期阶段
挂载过程在组件的一生中仅会发生一次,在这个过程中,组件被初始化,然后会被渲染到真实 DOM 里,完成所谓的“首次渲染”;
我们的App组件继承了react Component这个基类,才能使用render(),生命周期等方法;
super(props)用来调用基类的构造方法( constructor() ), 也将父组件的props注入给子组件;
constructor 方法,该方法仅仅在挂载的时候被调用一次,我们可以在该方法中对 this.state 进行初始化;
其中componentWillMount 会在执行 render 方法前被触发,一些同学习惯在这个方法里做一些初始化的操作,但这些操作往往会伴随一些风险或者说不必要性;
接下来 render 方法被触发。注意 render 在执行过程中并不会去操作真实 DOM(也就是说不会渲染),它的职能是把需要渲染的内容返回出来。真实 DOM 的渲染工作,在挂载阶段是由 ReactDOM.render 来承接的;
componentDidMount 方法在渲染结束后被触发,此时因为真实 DOM 已经挂载到了页面上,我们可以在这个生命周期里执行真实 DOM 相关的操作。此外,类似于异步请求、数据初始化这样的操作也大可以放在这个生命周期来做;
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
}
componentWillMount(){
console.log('componentWillMount 执行了')
}
render() {
console.log('render函数执行了')
return (
生命周期-组件的挂载
)
}
componentDidMount() {
console.log('组件已经挂载')
}
}
组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新;
根据生命周期图谱,父组件触发的更新和组件自身的更新相比,多出了这样一个生命周期方法componentWillReceiveProps (nextProps)
在这个生命周期方法里,nextProps 表示的是接收到新 props 内容,而现有的 props (相对于 nextProps 的“旧 props”)我们可以通过
this.props 拿到,由此便能够感知到 props 的变化;
class About extends Component{
render(){
return(
About
)
}
componentDidUpdate(){
console.log('about 更新')
}
// 组件被卸载
componentWillUnmount(){
console.log('about 被卸载了')
}
componentWillReceiveProps(nextProps){
console.log('父组件触发的 About 更新')
}
}
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()。这样非常影响效率,且会损害性能。
static getDerivedStateFromProps不是componentWillMount的替代品
getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新
state,如果返回 null 则不更新任何内容;
getDerivedStateFromProps 这个 API,其设计的初衷不是试图替换掉 componentWillMount,而是试图替换掉componentWillReceiveProps,因此它有且仅有一个用途:即 state 的值在任何时候都取决于 props;
static getDerivedStateFromProps(props, state)
React 团队为了确保 getDerivedStateFromProps 这个生命周期的纯洁性,直接从命名层面约束了它的用途。所以,如果你不是出于这个目的来使用 getDerivedStateFromProps,原则上来说都是不符合规范的。
值得一提的是,getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。React 16 以提供特定生命周期的形式,对这类诉求提供了更直接的支持。
这个方法和 getDerivedStateFromProps 颇有几分神似,它们都强调了“我需要一个返回值”这回事。区别在于该函数的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息(例如,滚动位置);
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 (
{/* ...contents... */}
);
}
}
在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。
带着这个结论,我们再来看看 React 16 打算废弃的是哪些生命周期:
componentWillMount;
componentWillUpdate;
componentWillReceiveProps。
这些生命周期的共性,就是它们都处于 render 阶段,都可能重复被执行,而且由于这些 API 常年被滥用,它们在重复执行的过程中都存在着不可小觑的风险。
别的不说,说说我自己在团队 code review 中见过的“骚操作”吧。在“componentWill”开头的生命周期里,你习惯于做的事情可能包括但不限于:
setState();
fetch 发起异步请求;
操作真实 DOM。
这些操作的问题(或不必要性)包括但不限于以下 3 点:
(1)完全可以转移到其他生命周期(尤其是 componentDidxxx)里去做。
比如在 componentWillMount 里发起异步请求。很多同学因为太年轻,以为这样做就可以让异步请求回来得“早一点”,从而避免首次渲染白屏。
可惜你忘了,异步请求再怎么快也快不过(React 15 下)同步的生命周期。componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。这样做不仅没有达到你预想的目的,还会导致服务端渲染场景下的冗余请求等额外问题,得不偿失。
(2)在 Fiber 带来的异步渲染机制下,可能会导致非常严重的 Bug。
试想,假如你在 componentWillxxx 里发起了一个付款请求。由于 render 阶段里的生命周期都可以重复执行,在 componentWillxxx 被打断 + 重启多次后,就会发出多个付款请求。
比如说,这件商品单价只要 10 块钱,用户也只点击了一次付款。但实际却可能因为 componentWillxxx 被打断 + 重启多次而多次调用付款接口,最终付了 50 块钱;又或者你可能会习惯在 componentWillReceiveProps 里操作 DOM(比如说删除符合某个特征的元素),那么 componentWillReceiveProps 若是执行了两次,你可能就会一口气删掉两个符合该特征的元素。
结合上面的分析,我们再去思考 getDerivedStateFromProps 为何会在设计层面直接被约束为一个触碰不到 this 的静态方法,其背后的原因也就更加充分了——避免开发者触碰 this,就是在避免各种危险的骚操作。
(3)即使你没有开启异步,React 15 下也有不少人能把自己“玩死”。
比如在 componentWillReceiveProps 和 componentWillUpdate 里滥用 setState 导致重复渲染死循环的。
总的来说,React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。在这个改造的过程中,React 团队精益求精,针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。