壹 ❀ 引

在日常面试中,若对于了解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)

相对旧版生命周期,直觉上新版多了getDerivedStateFromPropsgetSnapshotBeforeUpdate两个钩子,以及少了componentWillMountcomponentWillReceivePropscomponentWillUpdate三个都带有will的钩子,原先有四个will,新版中只剩下一个即将卸载了,简单梳理下新版流程:

挂载阶段:constructor---->getDerivedStatedFromProps---->render---->componentDidMount

更新阶段:getDerivedStateFromProps---->shouldComponentUpdate---->render---->getSnapshotBeforeUpdate---->componentDidUpdate

卸载阶段:componentWillUnmount

在新版生命周期中getDerivedStateFromProps显得与render一样重要,贯穿了组件初次挂载,与后续的propsstate的更新,那么接下来我们来介绍这两个新钩子,作为我之前生命周期的补充。

叁 ❀ 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>
)
}
}

在上述例子中,我们传递了nameage给子组件,而子组件也有自己的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>
)
}
}

那么它有什么用呢?看生命周期图谱,它和componentDidUpdaterender夹在中间,其实它的核心作用就是在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之外,componentWillMountcomponentWillReceivePropscomponentWillUpdate这三个钩子均被废弃。说废弃也不是现在直接不能用了,在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中可能需要做的事,constructorcomponentDidMount也能做,甚至做的更好,此方法被废弃。

componentWillReceiveProps实际行为与命名并不相符,由于不稳定性已由getDerivedStateFromProps代替;而componentWillUpdate同等理由被getSnapshotBeforeUpdate代替,至此将来时的三位成员纷纷退出历史舞台。

伍 ❀ 总

好了,本文到这里我们不仅介绍了新旧生命周期的区别,并补全了之前生命周期文章没介绍新钩子用法的遗憾,现在我们应该很清楚新版生命周期新增了以及废弃哪些钩子,回到文中提到的面试题,我想大家应该都能很好的组织语言回答这个问题,那么到这里本文结束。

react 新旧生命周期有什么区别?新增了哪些钩子?废弃了哪些钩子?为什么废弃?的更多相关文章

  1. react新旧生命周期

    React16.3.0之前生命周期 16.3开始建议使用新的生命周期

  2. react新的生命周期

    一. react16当前生命周期 componentWillMountrender前,所以setState不会重新渲染,服务端渲染唯一调用,推荐用constructor代替之 render compo ...

  3. React组件和生命周期简介

        React 简介----React 是 Facebook 出品的一套颠覆式的前端开发类库.为什么说它是颠覆式的呢? 内存维护虚拟 DOM 对于传统的 DOM 维护,我们的步骤可能是:1.初始化 ...

  4. react学习小结(生命周期- 实例化时期 - 存在期- 销毁时期)

    react学习小结   本文是我学习react的阶段性小结,如果看官你是react资深玩家,那么还请就此打住移步他处,如果你想给一些建议和指导,那么还请轻拍~ 目前团队内对react的使用非常普遍,之 ...

  5. 2. React组件的生命周期

    2. React组件的生命周期 使用React开发时候用到最多的就是React的组件了,通过继承React.Component,加入constructor构造函数,实现Render方法即可.这当中Re ...

  6. Vue与React的异同 -生命周期

    vue的生命周期 创建前 beforeCreate 创建   create 挂载前 beforeMount 挂载 mounted 更新前 beforeUpdate 更新 updated 销毁前 bef ...

  7. React 组件的生命周期方法

    React 组件的生命周期方法 按渲染顺序: 1: componentWillMount() – 在渲染之前执行,在客户端和服务器端都会执行. 2: componentDidMount() – 仅在第 ...

  8. react第三单元(react组件的生命周期)

    第三单元(react组件的生命周期) #课程目标 灵活掌握react组件的生命周期以及组件的活动过程. 能够灵活使用react的生命周期 #知识点 react的类组件的生命周期分为三个阶段 实例期 存 ...

  9. React 之 组件生命周期

    React 之 组件生命周期 理解1) 组件对象从创建到死亡它会经历特定的生命周期阶段2) React组件对象包含一系列的勾子函数(生命周期回调函数), 在生命周期特定时刻回调3) 我们在定义组件时, ...

  10. 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

随机推荐

  1. Go 标准库之 io.Copy 和 ioutil.ReadAll

    1. go 标准库之 io.Copy 和 ioutil.ReadAll 1.1 介绍 go 标准库中通过 ioutil.ReadAll 实现数据流的读取,io.Copy 实现数据流的读取和写入. 那两 ...

  2. OpenStack 工作流组件: Mistral

    1 Mistral 简介 Mistral 是由 Mirantis 开发,贡献给 OpenStack 社区的工作流组件,它提供 Workflow As a Service 服务. 在计算机中通常处理的任 ...

  3. ApplicationContextAware 的理解和应用

    当我们在项目中获取某一个spring bean时,可以定义一个类,实现ApplicationContextAware  该接口,该接口可以加载获取到所有的 spring bean. package c ...

  4. spring-transaction源码分析(2)EnableTransactionManagement注解

    概述(Java doc) 该注解开启spring的注解驱动事务管理功能,通常标注在@Configuration类上面用于开启命令式事务管理或响应式事务管理. @Configuration @Enabl ...

  5. strlen和sizeof区别

    strlen 和 sizeof 是 C/C++ 中用于获取字符串长度或数据类型大小的两个不同的操作. 1.strlen: strlen 是在 C 语言和 C++ 中用于获取以空字符 ('\0') 结尾 ...

  6. Laravel : Eloquent 新增

             public function ormCreate()     {         #  1. 使用模型新增 ->save()         /*               ...

  7. [转帖]MySQL 8.0 以后的版本策略变化

    https://www.modb.pro/db/1717815842220630016 产品版本变更   从2023年7月18日开始,MySQL官网出现了一个新的版本 MySQL 8.1.0,直接改变 ...

  8. [转帖]Zookeeper集群搭建(3个节点为例)

    Zookeeper集群搭建 1.说明 本文用的linux版本:centos6,准备3台centos6虚拟机,给他们安装zookeeper,每一台的安装过程相同,你也可以安装一台,然后克隆出另外两台.主 ...

  9. [转帖]Mars II - Microarchitectures - Phytium

    https://en.wikichip.org/wiki/phytium/microarchitectures/mars_ii Edit Values Mars II µarch General In ...

  10. [专题]中立遭质疑,提价遭反对,ARM的生存难题怎么破?

    中立遭质疑,提价遭反对,ARM的生存难题怎么破? https://news.cnblogs.com/n/669715/ ARM税要提高.. RISC-V的机会? 文/黎文婕 来源:锌刻度(ID:znk ...