react 新旧生命周期有什么区别?新增了哪些钩子?废弃了哪些钩子?为什么废弃?
壹 ❀ 引
在日常面试中,若对于了解react
的同学而言,多多少少会被问到生命周期相关的问题,比如大致阐述生命周期的运作流程,以及每个钩子函数大致的作用,而我在两位出去面试的同事那里了解到,他们都遇到了react新版生命周期废弃了哪些钩子?为什么要废弃?
这个高频问题。
我在从零开始的react入门教程(六),一篇文章理解react组件生命周期一文中详细阐述了旧版生命周期中每个钩子函数的作用,而当时也有同学留言到我未提及react
新增的部分钩子的作用,那么本文正好对于旧版本生命周期做一次运转对比,补全新钩子的作用,同时站在面试题的角度梳理下我们该如何组织语言,那么本文开始。
贰 ❀ 新旧生命周期图谱对比
react
在版本16.3
前后存在两套生命周期,16.3
之前为旧版,之后则是新版,虽有新旧之分,但主体上大同小异。新版也只是废弃了三个不常用的钩子以及添加了两个依旧不怎么常用的新钩子,所以对于部分同学而言,即便你升级到了"新版",但出于业务场景需求,你可能不太需要使用新增的钩子,因此不了解新钩子对于日常开发还真没啥影响。
闲话不多说,我们先复习旧版生命周期,这里只是普及生命周期大致运作流程,详细使用请参考文中开头所提及的生命周期博客。
挂载阶段:constructor
---->componentWillMount
---->render
---->componentDidMount
更新阶段:componentWillReceiveProps
---->shouldComponentUpdate
---->render
---->componentDidUpdate
卸载阶段:componentWillUnmount
需要注意的是,上图中当setState
引发状态变化时,并不会经过componentWillReceiveProps
,而是直接触发shouldComponentUpdate
;而当触发forceUpdate
时,由于是强制更新,因此也会绕过是否应该更新的判断,而是直接走到componentWillUpdate
。
与即将挂载---->挂载完成,即将更新---->更新完成不同,卸载只有一个即将卸载,并没有卸载完成,react
有提供如下API用于卸载组件:
ReactDOM.unmountComponentAtNode(container)
由于是从DOM
中移除组件,因此这个方法是从ReactDOM
中获取。组件卸载后,组件定义的事件(event handlers)以及state
会被一并清除,但是像我们添加的事件监听,事件派发还是需要手动解绑,这也是为什么我们在开发中常常在componentWillUnmount
中解绑一些监听的缘故。下面这个例子演示移除组件操作以及componentWillUnmount
的执行:
class Echo extends Component {
componentWillUnmount(){
console.log('我被自己卸载了')
}
handlerUnmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
render() {
return (
<div className="parent">
<button onClick={this.handlerUnmount}>点我卸载自己</button>
</div>
)
}
}
但不要依赖这个卸载组件的API
,一般情况下,这个方法并没什么大作用,因为它能卸载的container
一般是我们挂在组件的容器,也就是不是写在react
中的dom
结构,比如现在我有一个想点击父组件方法,从而卸载子组件,你可能会想到这样写:
class Echo extends Component {
handlerUnmount = () => {
ReactDOM.unmountComponentAtNode(document.querySelector('.unmount'));
}
render() {
return (
<div className="parent">
<button onClick={this.handlerUnmount}>点我卸载下面的组件</button>
<div className="unmount">
<B />
</div>
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
我要被卸载了
</div>
)
}
}
但如果我们点击按钮执行卸载,在控制台可以看到如下警告:
警告也是在告知我们不能卸载由react
渲染提供的dom
节点。那我现在就是希望点击按钮隐藏组件B
怎么办?推荐的做法是通过控制子组件显示隐藏达到这个效果,而非真的卸载,比如:
class Echo extends Component {
state = {
isShowChild: true
}
handlerUnmount = () => {
this.setState({ isShowChild: false });
}
render() {
return (
<div className="parent">
<button onClick={this.handlerUnmount}>点我卸载下面的组件</button>
{
this.state.isShowChild ? <B /> : null
}
</div>
)
}
}
以上只是谈到了unmountComponentAtNode
的题外话,旧版生命周期大概如此,我们来看看新版生命周期(16.4):
相对旧版生命周期,直觉上新版多了getDerivedStateFromProps
与getSnapshotBeforeUpdate
两个钩子,以及少了componentWillMount
,componentWillReceiveProps
与componentWillUpdate
三个都带有will
的钩子,原先有四个will
,新版中只剩下一个即将卸载了,简单梳理下新版流程:
挂载阶段:constructor
---->getDerivedStatedFromProps
---->render
---->componentDidMount
更新阶段:getDerivedStateFromProps
---->shouldComponentUpdate
---->render
---->getSnapshotBeforeUpdate
---->componentDidUpdate
卸载阶段:componentWillUnmount
在新版生命周期中getDerivedStateFromProps
显得与render
一样重要,贯穿了组件初次挂载,与后续的props
、state
的更新,那么接下来我们来介绍这两个新钩子,作为我之前生命周期的补充。
叁 ❀ react新增的生命钩子
叁 ❀ 壹 getDerivedStateFromProps
derived [di'raivd] 衍生的,派生的
,那么翻译过来,这个钩子的作用其实就是从props
中获取衍生的state
,我们通过一个例子了解这个钩子的作用:
class Echo extends Component {
state={
name:'echo'
}
render() {
return (
<div className="parent">
<B name={this.state.name}/>
</div>
)
}
}
class B extends Component {
// 注意,声明此钩子必须添加static
static getDerivedStateFromProps(props) {
return props;
}
render() {
console.log(this.state)
return (
<div>
我的名字是:{this.state.name}
</div>
)
}
}
这个例子中,我们从父组件将this.state.name
作为props
传递给子组件,注意,子组件并没有声明state
,在getDerivedStateFromProps
中我们接受了父组件的props
同时返回,结果可以看到最终render
处输出的state
居然就是传递的props
。
先说结论,getDerivedStateFromProps
中返回一个对象用于更新当前组件的state
,比如上面的例子你没state
,那我直接就将返回的props
作为state
,那么假设我有自己的state
,且对象的key不一致会怎么样?看个例子:
class Echo extends Component {
state={
name:'echo',
age:17
}
render() {
return (
<div className="parent">
<B name={this.state.name} age={this.state.age}/>
</div>
)
}
}
class B extends Component {
state={
color:'red',
}
// 注意,声明此钩子必须添加static
static getDerivedStateFromProps(props) {
return props;
}
render() {
console.log(this.state)
return (
<div>
我的名字是:{this.state.name}
</div>
)
}
}
在上述例子中,我们传递了name
与age
给子组件,而子组件也有自己的state
,只是值是color
,在传递后我们发现并不是props
直接替代了子组件的state
,而是与现有子组件的state
进行了融合。
所以到这里我们能确定getDerivedStateFromProps
返回对象确实是更新当前组件的state
,而不是直接取代,假设你啥也没有,那直接用我的,如果你有那咱们就融合,同名的key
我帮你覆盖更新,没有的属性那就直接用我给你的,大概如此。
在了解了钩子作用后,可以很明确的说,这个钩子确实没啥大作用,官网也说了,除非你有props
永远都作为子组件state
的场景,不然一般你也用不上它,即便有这个场景,我们不用这个钩子一样能实现,所以这个钩子基本没啥存在感。
叁 ❀ 贰 getSnapshotBeforeUpdate
snapshot [ˈsnæpʃɒt] 快照
,这个钩子的意思其实就是在组件更新前获取快照,此方法一般结合componentDidUpdate
使用,getSnapshotBeforeUpdate
中返回的值将作为第三参数传递给componentDidUpdate
,一个最简单的例子:
class Echo extends Component {
state = {
name: 'echo'
}
getSnapshotBeforeUpdate() {
return 1;
}
componentDidUpdate(preProps, preState, snapshot) {
console.log(preProps, preState, snapshot);
}
handlerClick=()=>{
this.setState({name:'听风是风'});
}
render() {
return (
<div>
<button onClick={this.handlerClick}>点我</button>
{this.state.name}
</div>
)
}
}
那么它有什么用呢?看生命周期图谱,它和componentDidUpdate
将render
夹在中间,其实它的核心作用就是在render
改变dom
之前,记录更新前的dom
信息传递给componentDidUpdate
。为了更好的理解这个钩子,我们来模拟实现简陋的消息查看系统,来看个例子:
class Echo extends Component {
state = {
messageList: []
}
ulRef = React.createRef();
componentDidMount() {
setInterval(() => {
const { messageList } = this.state;
const newMessage = `新消息${messageList.length + 1}`;
this.setState({ messageList: [newMessage, ...messageList] })
}, 1000);
}
render() {
return (
<ul ref={this.ulRef}>
{
this.state.messageList.map((message, index) => (
<li key={index}>{message}</li>
))
}
</ul>
)
}
}
ul {
margin: 20px;
border: 1px solid #000000;
width: 180px;
height: 150px;
list-style: none;
overflow: auto;
li {
height: 25px;
}
}
在这个例子,我们用一个定时器每隔一秒模拟新增一条新消息,且新消息会不断把旧有消息往下顶,所以这就造成即便我们往下滚动想看之前的消息还是被新消息感染,现在我想达到新消息还是不断新增,但窗口相对静止不妨碍我看之前的新闻,那么这里就能结合getSnapshotBeforeUpdate
达到这个效果,我们在上述代码中增加如下两个钩子:
getSnapshotBeforeUpdate() {
// 获取渲染之前的ul的内容区域高度
const preScrollHeight = this.ulRef.current.scrollHeight;
return preScrollHeight
}
componentDidUpdate(preProps, preState, preScrollHeight) {
// 使用渲染后的新内容高度减去旧内容区域的高度,其实就是一个li的高度,并累加给scrollTop,让滚动条达到相对静止
this.ulRef.current.scrollTop += this.ulRef.current.scrollHeight - preScrollHeight;
}
原理其实很简单,就是你增加一个li
的高度,我就让我当前的scrollTop
也自增加上一个li
的高度,达到当前视图区域相对静止,由于是已知li
的高度,有的同学可能已经想到其实根本不需要getSnapshotBeforeUpdate
获取旧有ul
的内容高度,直接删掉getSnapshotBeforeUpdate
并修改componentDidUpdate
为:
componentDidUpdate() {
// 这个例子中我们已知一个li固定高25px
this.ulRef.current.scrollTop += 25;
}
其实也没错,但实际场景中,不同人可能给你发个表情,也可能发一大段的文字,li
的高度并不固定,所以上述获取旧有内容区域高度的做法还是有场景需要的。
那么到这里我们也介绍完了getSnapshotBeforeUpdate
,虽然看上去实用的场景也不多,但如果真的有需要获取旧有dom
的信息,希望你能记起它。
肆 ❀ react废弃了哪些钩子?为什么
在介绍完新版生命周期中的钩子,其实我们也清楚了废弃了哪些旧有钩子,react
一共四个will
将来时的钩子,除了componentWillUnmout
之外,componentWillMount
、componentWillReceiveProps
、componentWillUpdate
这三个钩子均被废弃。说废弃也不是现在直接不能用了,在react 17
版本中如果我们用了上述写法,官方会给出警告并推荐我们在这三个钩子前添加UNSAFE_
前缀,比如UNSAFE_componentWillMount
,且官方强调预计在后续版本可能只支持UNSAFE_
前缀写法。
那为什么要废弃这三个呢?react
中生命周期钩子虽然多,但事实上常用的就那么几个,比如新版废弃的钩子中可能除了componentWillReceiveProps
常用一点外,另外两个使用率并不太高。按照官方的说法,这三个钩子很容易被误解和滥用,而且在未来react
打算提供异步渲染能力,那么这几个钩子的不稳定很可能被放大,从而带来不可预测的bug
。
当然,上述是官方的说法,我们可以站在实际使用角度聊聊这三个钩子所带来的疑问。
肆 ❀ 壹 关于componentWillReceiveProps
我们前面说componentWillReceiveProps
用的还比较多,那么这个钩子的含义是什么?什么时候下触发?是组件即将接收props
触发?还是即将接收新props
时触发?我们来看个例子:
class Echo extends Component {
state = {
name: '听风是风',
age:18
}
changeAge = () => {
this.setState({age:28})
}
render() {
return (
<div>
<button onClick={this.changeAge}>改变年龄</button>
<B name={this.state.name}/>
</div>
)
}
}
class B extends Component {
componentWillReceiveProps(){
console.log(1)
}
render() {
return (
<div>
我的名字是:{this.props.name}
</div>
)
}
}
比如上述例子中,初次渲染父组件给子组件传递了name
属性,但子组件初次渲染并不会触发componentWillReceiveProps
;而当我们改变父组件状态从而触发子组件再次渲染,这时候子组件的props
其实没改变,但componentWillReceiveProps
又被触发了。
所以componentWillReceiveProps
触发的机制其实是除了初次渲染,之后只要父组件再次渲染,不管props
有发生改变都会触发子组件的componentWillReceiveProps
,现在你觉得这个钩子叫这个名合理吗。它其实并没有按照它命名的意思去执行,虽然大多数情况下我们喜欢在这里比较新旧props
,若发生了变化就去更新子组件的state
,但我们仔细一想,新增的getDerivedStateFromProps
不也可以达到这个效果吗,而且它在初次渲染或者后续更新都能保证执行,更为稳定。
肆 ❀ 贰 关于componentWillMount
接触react
稍微久一点的同学都知道,若一个组件需要请求数据,那么这个请求应该放在componentDidMout
,但可能不少同学一开始都有过这样的疑惑,为什么不能将请求放在componentWillMount
中呢?理论上来说,即将挂载就开始请求,早请求数据早回来,那这样还能减少数据未返回的白屏时间。
想法是好的,但这个优化的实际效果却是微乎其微的,而且假设我们有做服务端渲染,componentWillMount
会在服务端以及前端各自执行一次,但如果在didmount
中请求,则只会在前端请求一次。而且由于后期react
引入fiber
的概念,react
中的任务也有了优先级之分,而在render
之前的任务,极有可能被更高优先级的任务打断,导致多次执行,这也是为什么react
一次性废弃了三个render
之前will
类型钩子的原因之一,至于willUnmount
,这玩意就跟组件要去世了,走之前交代后事,也没有后续render
的可能性,所以留着不会有啥影响。
当然,也有同学会说,那我还是想在willMount
中初始化定义一些预加载的数据,但别忘了我们还有constructor
,一些数据初始化的操作就应该放在这个钩子中处理。所以这样说下来,我们会发现willMount
的定义太模糊了,它能干的事另外两个钩子都能代劳,那么留一个让开发者疑惑的钩子有何意义了,自然被干掉了。
肆 ❀ 叁 关于componentWillUpdate
这个钩子其实在用法上与componentWillReceiveProps
类似,可能也有同学习惯在这个钩子中做新旧props
对比,从而调用一些callback
之类,当然,从含义上来说,组件即将更新,所以也会有在这个钩子中做更新前dom
获取操作的行为;但与componentWillReceiveProps
类似,这个钩子也可能因为不合理的用法导致这个钩子被调用多次;其次,考虑到获取更新前dom
的需求,react
提供了一个更为稳定的新钩子getSnapshotBeforeUpdate
,这个方法我们在之前已经演示过了。
总结来说,componentWillMount
中可能需要做的事,constructor
与componentDidMount
也能做,甚至做的更好,此方法被废弃。
componentWillReceiveProps
实际行为与命名并不相符,由于不稳定性已由getDerivedStateFromProps
代替;而componentWillUpdate
同等理由被getSnapshotBeforeUpdate
代替,至此将来时的三位成员纷纷退出历史舞台。
伍 ❀ 总
好了,本文到这里我们不仅介绍了新旧生命周期的区别,并补全了之前生命周期文章没介绍新钩子用法的遗憾,现在我们应该很清楚新版生命周期新增了以及废弃哪些钩子,回到文中提到的面试题,我想大家应该都能很好的组织语言回答这个问题,那么到这里本文结束。