React生命周期详解

简介: React生命周期详解
React生命周期

所谓的React生命周期,就是指组件从被创建出来,到被使用,最后被销毁的这么一个过程;

而在这个过程中,React提供了我们会自动执行的不同的钩子函数,我们称之为生命周期函数;

**组件的生命周期大致分为三个阶段:组件挂载阶段,组件更新阶段,组件销毁卸载阶段 **

16.4之后的声明周期图谱如下:

组件初始化阶段

由ReactDOM.render()触发—初次渲染

  1. constructor()
  2. getDerivedStateFromProps
  3. render()
  4. componentDidMount()

组件更新阶段

由组件内部this.setState()或者父组件重新render触发或者forceUpdate()

  1. getDerivedStateFromProps
  2. shouldComponentUpdate()
  3. render()
  4. getSnapshotBeforeUpdate()
  5. componentDidUpdate()

组件卸载阶段

由ReactDOM.unmountComponentAtNode()触发

  1. componentWillUnmount()

React 15 生命周期和 React 16.3 生命周期在挂载阶段的主要差异在于,废弃了 componentWillMount,新增了 getDerivedStateFromProps。废弃了 componentWillUpdate() 新增了 getSnapshotBeforeUpdate

而在16以前的声明周期图谱如下:

初始化阶段

由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 (
            <div>
              <h3>生命周期-组件的挂载</h3>
            </div>
        )
    }
    componentDidMount() {
        console.log('组件已经挂载')
    }
}
组件更新阶段

组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新;

根据生命周期图谱,父组件触发的更新和组件自身的更新相比,多出了这样一个生命周期方法componentWillReceiveProps (nextProps)

在这个生命周期方法里,nextProps 表示的是接收到新 props 内容,而现有的 props (相对于 nextProps 的“旧 props”)我们可以通过

this.props 拿到,由此便能够感知到 props 的变化;

 

class About extends Component{
    render(){
        return(
            <h3>About</h3>
        )
    }
    componentDidUpdate(){
        console.log('about 更新')
    }
    // 组件被卸载
    componentWillUnmount(){
        console.log('about 被卸载了')
    }
    componentWillReceiveProps(nextProps){
        console.log('父组件触发的 About 更新')
    }
}
shouldComponentUpdate方法
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)
  • getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部访问不到 this
  • 该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state,如果需要修改state的值可以直接像state.count = 10 这样使用;
  • getDerivedStateFromProps 需要一个对象格式的返回值;

React 团队为了确保 getDerivedStateFromProps 这个生命周期的纯洁性,直接从命名层面约束了它的用途。所以,如果你不是出于这个目的来使用 getDerivedStateFromProps,原则上来说都是不符合规范的。

值得一提的是,getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。React 16 以提供特定生命周期的形式,对这类诉求提供了更直接的支持。

 

消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate

这个方法和 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 (
                 <div ref={this.listRef}>{/* ...contents... */}</div>
            );
    }
}
React生命周期为什么做如此改变?

在 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 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。

目录
相关文章
|
6天前
|
前端开发
react生命周期的一些坑
react生命周期的一些坑
|
6天前
|
前端开发 JavaScript 开发者
【第27期】一文了解React生命周期
【第27期】一文了解React生命周期
35 0
|
6天前
|
前端开发
React中生命周期的讲解
React中生命周期的讲解
|
6天前
|
前端开发 JavaScript
react 生命周期讲解
react 生命周期讲解
|
6天前
|
前端开发 JavaScript
React生命周期方法在实际开发中的应用场景有哪些?
【4月更文挑战第6天】 React 生命周期方法应用于数据获取、订阅管理、渲染逻辑处理、用户交互响应、性能优化、资源清理、强制更新、错误处理、动画实现、代码分割、服务端渲染、路由处理、依赖注入和集成第三方库。它们帮助控制组件行为和性能,但现代开发推荐使用Hooks替代部分生命周期方法。
18 0
|
6天前
|
前端开发 JavaScript
react生命周期函数
react生命周期函数
11 0
|
6天前
|
前端开发 JavaScript 开发者
浅谈React生命周期
浅谈React生命周期
13 0
|
6天前
|
前端开发
React旧有生命周期和新生命周期的解析
React旧有生命周期和新生命周期的解析
31 0
React旧有生命周期和新生命周期的解析
|
6天前
|
前端开发 JavaScript API
React组件生命周期
React组件生命周期
78 1
|
6天前
|
前端开发 JavaScript 测试技术
第十三章 React生命周期(新)
第十三章 React生命周期(新)

热门文章

最新文章