听风是风

学或不学,知识都在那里,只增不减。

导航

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)

相对旧版生命周期,直觉上新版多了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代替,至此将来时的三位成员纷纷退出历史舞台。

伍 ❀ 总

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

posted on 2021-12-09 17:10  听风是风  阅读(656)  评论(0编辑  收藏  举报