生命周期
# React
的生命周期有哪些?
React
通常将组件生命周期分为三个阶段:
- 装载阶段(
Mount
),组件第一次在DOM
树中被渲染的过程; - 更新过程(
Update
),组件状态发生变化,重新更新渲染的过程; - 卸载过程(
Unmount
),组件从DOM
树中被移除的过程;
# 1. 组件挂载阶段
挂载阶段组件被创建,然后组件实例插入到 DOM
中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:
constructor
getDerivedStateFromProps
render
componentDidMount
constructor
组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props)
,否则无法在构造函数中拿到this
。
如果不初始化 state
或不进行方法绑定,则不需要为 React
组件实现构造函数**Constructor
**。
constructor
中通常只做两件事:
- 初始化组件的
state
- 给事件处理方法绑定
this
constructor(props) {
super(props);
// 不要在构造函数中调用 setState,可以直接给 state 设置初始值
this.state = { counter: 0 }
this.handleClick = this.handleClick.bind(this)
}
2
3
4
5
6
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
这是个静态方法,所以不能在这个函数里使用 this
,有两个参数 props
和 state
,分别指接收到的新参数和当前组件的 state
对象,这个函数会返回一个对象用来更新当前的 state
对象,如果不需要更新可以返回 null
。
该函数会在装载时,接收到新的 props
或者调用了 setState
和 forceUpdate
时被调用。如当接收到新的属性想修改 state
,就可以使用。
// 当 props.counter 变化时,赋值给 state
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
static getDerivedStateFromProps(props, state) {
if (props.counter !== state.counter) {
return {
counter: props.counter
}
}
return null
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
</div>
)
}
}
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
30
现在可以显式传入 counter
,但是这里有个问题,如果想要通过点击实现 state.counter
的增加,但这时会发现值不会发生任何变化,一直保持 props
传进来的值。这是由于在 React 16.4+
的版本中 setState
和 forceUpdate
也会触发这个生命周期,所以当组件内部 state
变化后,就会重新走这个方法,同时会把 state
值赋值为 props
的值。因此需要多加一个字段来记录之前的 props
值,这样就会解决上述问题。具体如下:
// 这里只列出需要变化的地方
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
// 增加一个 preCounter 来记录之前的 props 传来的值
preCounter: 0,
counter: 0
}
}
static getDerivedStateFromProps(props, state) {
// 跟 state.preCounter 进行比较
if (props.counter !== state.preCounter) {
return {
counter: props.counter,
preCounter: props.counter
}
}
return null
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
</div>
)
}
}
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
30
31
32
33
render
render
是React
中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state
和属性 props
渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:
React
元素:这里包括原生的DOM
以及React
组件;- 数组和
Fragment
(片段):可以返回多个元素; Portals
(传送门):可以将子元素渲染到不同的DOM
子树种;- 字符串和数字:被渲染成
DOM
中的text
节点; - 布尔值或
null
:不渲染任何内容。
componentDidMount
componentDidMount()
会在组件挂载后(插入 DOM
树中)立即调。该阶段通常进行以下操作:
- 执行依赖于
DOM
的操作; - 发送网络请求;(官方建议)
- 添加订阅消息(会在
componentWillUnmount
取消订阅);
如果在 componentDidMount
中调用 setState
,就会触发一次额外的渲染,多调用了一次 render
函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此是没有感知的,但是我应当避免这样使用,这样会带来一定的性能问题,尽量是在 constructor
中初始化 state
对象。
在组件装载之后,将计数数字变为1:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
componentDidMount () {
this.setState({
counter: 1
})
}
render () {
return (
<div className="counter">
counter值: { this.state.counter }
</div>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. 组件更新阶段
当组件的 props
改变了,或组件内部调用了 setState/forceUpdate
,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:
getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
在说这个生命周期函数之前,来看两个问题:
setState
函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:
this.setState({number: this.state.number})
- 如果没有调用
setState
,props
值也没有变化,是不是组件就不会重新渲染?
第一个问题答案是 会 ,第二个问题如果是父组件重新渲染时,不管传入的 props
有没有变化,都会引起子组件的重新渲染。
那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候 shouldComponentUpdate
登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回 true
,可以比较 this.props
和 nextProps
,this.state
和 nextState
值是否变化,来确认返回 true 或者 false
。当返回 false
时,组件的更新过程停止,后续的 render
、componentDidUpdate
也不会被调用。
注意: 添加 shouldComponentUpdate
方法时,不建议使用深度相等检查(如使用 JSON.stringify()
),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
这个方法在 render
之后,componentDidUpdate
之前调用,有两个参数 prevProps
和 prevState
,表示更新之前的 props
和 state
,这个函数必须要和 componentDidUpdate
一起使用,并且要有一个返回值,默认是 null
,这个返回值作为第三个参数传给 componentDidUpdate
。
componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法。 该阶段通常进行以下操作:
- 当组件更新后,对
DOM
进行操作; - 如果你对更新前后的
props
进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
componentDidUpdate(prevProps, prevState, snapshot){}
该方法有三个参数:
prevProps
: 更新前的props
prevState
: 更新前的state
snapshot: getSnapshotBeforeUpdate()
生命周期的返回值
# 3. 组件卸载阶段
卸载阶段只有一个生命周期函数,componentWillUnmount()
会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作:
- 清除 timer,取消网络请求或清除
- 取消在 componentDidMount() 中创建的订阅等;
这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState
,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。
# 4. 错误处理阶段
componentDidCatch(error, info)
,此生命周期在后代组件抛出错误后被调用。 它接收两个参数∶
error
:抛出的错误。info
:带有componentStack key
的对象,其中包含有关组件引发错误的栈信息
# React
废弃了哪些生命周期?为什么?
被废弃的三个函数都是在render
之前,因为fiber
的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外的一个原因则是,React
想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及即将废弃的生命周期分析入手
componentWillMount
首先这个函数的功能完全可以使用componentDidMount
和 constructor
来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在constructor
中执行,除此之外,如果在 willMount
中订阅事件,但在服务端这并不会执行 willUnMount
事件,也就是说服务端会导致内存泄漏所以 componentWilIMount
完全可以不使用,但使用者有时候难免因为各种各样的情况在 componentWilMount
中做一些操作,那么React
为了约束开发者,干脆就抛掉了这个API
componentWillReceiveProps
在老版本的 React
中,如果组件自身的某个 state
跟其 props
密切相关的话,一直都没有一种很优雅的处理方式去更新 state
,而是需要在 componentWilReceiveProps
中判断前后两个 props
是否相同,如果不同再将新的 props
更新到相应的 state
上去。这样做一来会破坏 state
数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab
显然隶属于列表自身的时,根据传入的某个值,直接定位到某个 Tab
。为了解决这些问题,React
引入了第一个新的生命周期:getDerivedStateFromProps
。它有以下的优点∶
getDSFP
是静态方法,在这里不能使用this
,也就是一个纯函数,开发者不能写出副作用的代码- 开发者只能通过
prevState
而不是prevProps
来做对比,保证了state
和props
之间的简单关系以及不需要处理第一次渲染时prevProps
为空的情况 - 基于第一点,将状态变化(
setState
)和昂贵操作(tabChange
)区分开,更加便于render
和commit
阶段操作或者说优化。
componentWillUpdate
与 componentWillReceiveProps
类似,许多开发者也会在 componentWillUpdate
中根据 props
的变化去触发一些回调 。 但不论是 componentWilReceiveProps
还 是 componentWilUpdate
,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount
类似, componentDidUpdate
也不存在这样的问题,一次更新中 componentDidUpdate
只会被调用一次,所以将原先写在 componentWillUpdate
中的回调迁移至 componentDidUpdate
就可以解决这个问题。
另外一种情况则是需要获取DOM
元素状态,但是由于在fiber
中,render
可打断,可能在wilMount
中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决 getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate(prevProps, prevState)
返回的值作为componentDidUpdate
的第三个参数。与willMount
不同的是,getSnapshotBeforeUpdate
会在最终确定的render
执行之前执行,也就是能保证其获取到的元素状态与didUpdate
中获取到的元素状态相同。官方参考代码:
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 (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
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
30
31
32
# React 16.X
中 props
改变后在哪个生命周期中处理
在getDerivedStateFromProps
中进行处理。
这个生命周期函数是为了替代componentWillReceiveProps
存在的,所以在需要使用componentWillReceiveProps
时,就可以考虑使用getDerivedStateFromProps
来进行替代。
两者的参数是不相同的,而getDerivedStateFromProps
是一个静态函数,也就是这个函数不能通过this
访问到class
的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps
以及prevState
来进行判断,根据新传入的props
来映射到state
。
需要注意的是,如果props
传入的内容不需要影响到你的state
,那么就需要返回一个null
,这个返回值是必须的,所以尽量将其写到函数的末尾:
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
2
3
4
5
6
7
8
9
10
11
# React
性能优化在哪个生命周期?它优化的原理是什么?
react
的父级组件的render
函数重新渲染会引起子组件的render
方法的重新渲染。但是,有的时候子组件的接受父组件的数据没有变动。子组件render
的执行会影响性能,这时就可以使用shouldComponentUpdate
来解决这个问题。
使用方法如下:
shouldComponentUpdate(nexrProps) {
if (this.props.num === nexrProps.num) {
return false
}
return true;
}
2
3
4
5
6
shouldComponentUpdate
提供了两个参数nextProps
和nextState
,表示下一次props
和一次state
的值,当函数返回false
时候,render()
方法不执行,组件也就不会渲染,返回true
时,组件照常重渲染。此方法就是拿当前props
中值和下一次props
中的值进行对比,数据相等时,返回false
,反之返回true
。
需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为true
。
面对这个问题,可以使用如下方法进行解决:
- 使用
setState
改变数据之前,先采用ES6
中assgin
进行拷贝,但是assgin
只深拷贝的数据的第一层,所以说不是最完美的解决办法:
const o2 = Object.assign({},this.state.obj)
o2.student.count = '00000';
this.setState({
obj: o2,
})
2
3
4
5
- 使用
JSON.parse(JSON.stringfy())
进行深拷贝,但是遇到数据为undefined
和函数时就会错。
const o2 = JSON.parse(JSON.stringify(this.state.obj))
o2.student.count = '00000';
this.setState({
obj: o2,
})
2
3
4
5
# state
和 props
触发更新的生命周期分别有什么区别?
state
更新流程: 这个过程当中涉及的函数:
shouldComponentUpdate
: 当组件的state
或props
发生改变时,都会首先触发这个生命周期函数。它会接收两个参数:nextProps
,nextState
——它们分别代表传入的新props
和新的state
值。拿到这两个值之后,我们就可以通过一些对比逻辑来决定是否有re-render
(重渲染)的必要了。如果该函数的返回值为false
,则生命周期终止,反之继续;
注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生
bug
。应该考虑使用内置的PureComponent
组件,而不是手动编写shouldComponentUpdate()
componentWillUpdate
:当组件的state
或props
发生改变时,会在渲染之前调用componentWillUpdate
。componentWillUpdate
是React16
废弃的三个生命周期之一。过去,我们可能希望能在这个阶段去收集一些必要的信息(比如更新前的DOM
信息等等),现在我们完全可以在React16
的getSnapshotBeforeUpdate
中去做这些事;componentDidUpdate
:componentDidUpdate()
会在UI
更新后会被立即调用。它接收prevProps
(上一次的props
值)作为入参,也就是说在此处我们仍然可以进行props
值对比(再次说明componentWillUpdate
确实鸡肋哈)。
props
更新流程: 相对于
state
更新,props
更新后唯一的区别是增加了对 componentWillReceiveProps
的调用。关于 componentWillReceiveProps
,需要知道这些事情:
componentWillReceiveProps
:它在Component
接受到新的props
时被触发。componentWillReceiveProps
会接收一个名为nextProps
的参数(对应新的props
值)。该生命周期是React16
废弃掉的三个生命周期之一。在它被废弃前,可以用它来比较this.props
和nextProps
来重新setState
。在React16
中,用一个类似的新生命周期getDerivedStateFromProps
来代替它。
# React
中发起网络请求应该在哪个生命周期中进行?为什么?
对于异步请求,最好放在componentDidMount
中去操作,对于同步的状态改变,可以放在componentWillMount
中,一般用的比较少。
如果认为在componentWillMount
里发起请求能提早获得结果,这种想法其实是错误的,通常componentWillMount
比componentDidMount
早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。
react
的生命周期: constructor()
-> componentWillMount()
-> render()
-> componentDidMount()
上面这些方法的调用是有次序的,由上而下依次调用。
constructor
被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。componentWillMount
方法的调用在constructor
之后,在render
之前,在这方法里的代码调用setState
方法不会触发重新render
,所以它一般不会用来作加载数据之用。componentDidMount
方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用setState
方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在constructor
里做,但constructor
是做组件state
初绐化工作,并不是做加载数据这工作的,constructor
里也不能setState
,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在componentDidMount
方法里。
总结:
- 跟服务器端渲染(同构)有关系,如果在
componentWillMount
里面获取数据,fetch data
会执行两次,一次在服务器端一次在客户端。在componentDidMount
中可以解决这个问题,componentWillMount
同样也会render
两次。 - 在
componentWillMount
中fetch data
,数据一定在render
后才能到达,如果忘记了设置初始状态,用户体验不好。 react16.0
以后,componentWillMount
可能会被执行多次。
# React 16
中新生命周期有哪些
关于 React16
开始应用的新生命周期: 可以看出,
React16
自上而下地对生命周期做了另一种维度的解读:
Render
阶段:用于计算一些必要的状态信息。这个阶段可能会被React
暂停,这一点和React16
引入的Fiber
架构(我们后面会重点讲解)是有关的;Pre-commit
阶段:所谓“commit
”,这里指的是“更新真正的DOM
节点”这个动作。所谓Pre-commit
,就是说我在这个阶段其实还并没有去更新真实的DOM
,不过DOM
信息已经是可以读取的了;Commit
阶段:在这一步,React
会完成真实DOM
的更新工作。Commit
阶段,我们可以拿到真实DOM
(包括refs
)。
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:
- 挂载过程:
constructor
getDerivedStateFromProps
render
componentDidMount
- 更新过程:
getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
- 卸载过程:
componentWillUnmount