React专题:生命周期

6,146 阅读12分钟

本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出

来我的 GitHub repo 阅读完整的专题文章

来我的 个人博客 获得无与伦比的阅读体验

生命周期,顾名思义,就是从生到死的过程。

而生命周期钩子,就是从生到死过程中的关键节点。

普通人的一生有哪些生命周期钩子呢?

  • 出生
  • 考上大学
  • 第一份工作
  • 买房
  • 结婚
  • 生子
  • 孩子的生命周期钩子
  • 退休
  • 临终遗言

每到关键节点,我们总希望有一些沉思时刻,因为这时候做出的决策会改变人生的走向。

React组件也一样,它会给开发者一些沉思时刻,在这里,开发者可以改变组件的走向。

异步渲染下的生命周期

React花了两年时间祭出Fiber渲染机制。

简单来说,React将diff的过程叫做Reconciliation。以前这一过程是一气呵成的,Fiber机制把它改成了异步。异步技能将在接下来的版本中逐步解锁。

明明是一段同步代码,怎么就异步了呢?

原理是Fiber把任务切成很小的片,每执行一片就把控制权交还给主线程,待主线程忙完手头的活再来执行剩下的任务。当然如果某一片的执行时间就很长(比如死循环),那就没主线程什么事了,该崩溃崩溃。

这会给生命周期带来什么影响呢?

影响就是挂载和更新之前的生命周期都变的不可靠了。

为什么这么讲?因为Reconciliation这个过程有可能暂停然后继续执行,所以挂载和更新之前的生命周期钩子就有可能不执行或者多次执行,它的表现是不可预期的。

因此16之后的React生命周期迎来了一波大换血,以下生命周期钩子将被逐渐废弃:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

看出特点了么,都是带有will的钩子。

目前React为这几个生命周期钩子提供了别名,分别是:

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

React17将只提供别名,彻底废弃这三个大活宝。取这么个别名意思就是让你用着恶心。

constructor()

React借用class类的constructor充当初始化钩子。

React几乎没做什么手脚,但是因为我们只允许通过特定的途径给组件传递参数,所以constructor的参数实际上是被React规定好的。

React规定constructor有三个参数,分别是propscontextupdater

  • props是属性,它是不可变的。
  • context是全局上下文。
  • updater是包含一些更新方法的对象,this.setState最终调用的是this.updater.enqueueSetState方法,this.forceUpdate最终调用的是this.updater.enqueueForceUpdate方法,所以这些API更多是React内部使用,暴露出来是以备开发者不时之需。

在React中,因为所有class组件都要继承自Component类或者PureComponent类,因此和原生class写法一样,要在constructor里首先调用super方法,才能获得this

constructor生命周期钩子的最佳实践是在这里初始化this.state

当然,你也可以使用属性初始化器来代替,如下:

import React, { Component } from 'react';

class App extends Component {
    state = {
        name: 'biu',
    };
}

export default App;

componentWillMount()

💀这是React不再推荐使用的API。

这是组件挂载到DOM之前的生命周期钩子。

很多人会有一个误区:这个钩子是请求数据然后将数据插入元素一同挂载的最佳时机。

其实componentWillMount和挂载是同步执行的,意味着执行完这个钩子,立即挂载。而向服务器请求数据是异步执行的。所以无论请求怎么快,都要排在同步任务之后再处理,这是辈分问题。

也就是说,永远不可能在这里将数据插入元素一同挂载。

并不是说不能在这里请求数据,而是达不到你臆想的效果。

它被废弃的原因主要有两点:

  • 本来它就没什么用。估计当初是为了成双成对所以才创造了它吧。
  • 如果它声明了定时器或者订阅器,在服务端渲染中,componentWillUnmount生命周期钩子中的清除代码不会生效。因为如果组件没有挂载成功,componentWillUnmount是不会执行的。姚明说的:没有挂载就没有卸载。
  • 在异步渲染中,它的表现不稳定。

初始化this.state应该在constructor生命周期钩子中完成,请求数据应该在componentDidMount生命周期钩子中完成,所以它不仅被废弃了,连继任者都没有。

static getDerivedStateFromProps(props, state)

👽这是React v16.3.0发布的API。

首先,这是一个静态方法生命周期钩子。

也就是说,定义的时候得在方法前加一个static关键字,或者直接挂载到class类上。

简要区分一下实例方法和静态方法:

  • 实例方法,挂载在this上或者挂载在prototype上,class类不能直接访问该方法,使用new关键字实例化之后,实例可以访问该方法。
  • 静态方法,直接挂载在class类上,或者使用新的关键字static,实例无法直接访问该方法。

问题是,为什么getDerivedStateFromProps生命周期钩子要设计成静态方法呢?

这样开发者就访问不到this也就是实例了,也就不能在里面调用实例方法或者setsState了。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <div>React</div>
        );
    }

    static getDerivedStateFromProps(props, state) {}
}

export default App;

这个生命周期钩子的使命是根据父组件传来的props按需更新自己的state,这种state叫做衍生state。返回的对象就是要增量更新的state。

它被设计成静态方法的目的是保持该方法的纯粹,它就是用来定义衍生state的,除此之外不应该在里面执行任何操作。

这个生命周期钩子也经历了一些波折,原本它是被设计成初始化父组件更新接收到props才会触发,现在只要渲染就会触发,也就是初始化更新阶段都会触发。

render()

作为一个组件,最核心的功能就是把元素挂载到DOM上,所以render生命周期钩子是一定会用到的。

render生命周期钩子怎么接收模板呢?当然是你return给它。

但是不推荐在return之前写过多的逻辑,如果逻辑过多,可以封装成一个函数。

render() {
    // 这里可以写一些逻辑
    return (
        <div>
            <input type="text" />
            <button>click</button>
        </div>
    );
}

注意,千万不要在render生命周期钩子里调用this.setState,因为this.setState会引发render,这下就没完没了了。主公,有内奸。

componentDidMount()

这是组件挂载到DOM之后的生命周期钩子。

这可能是除了render之外最重要的生命周期钩子,因为这时候组件的各方面都准备就绪,天地任你闯。

这就是社会哥,人狠话不多。

componentWillReceiveProps(nextProps)

💀这是React不再推荐使用的API。

componentWillReceiveProps生命周期钩子只有一个参数,更新后的props。

该声明周期函数可能在两种情况下被触发:

  • 组件接收到了新的属性。
  • 组件没有收到新的属性,但是由于父组件重新渲染导致当前组件也被重新渲染。

初始化时并不会触发该生命周期钩子。

同样,因为Fiber机制的引入,这个生命周期钩子有可能会多次触发。

shouldComponentUpdate(nextProps, nextState)

这个生命周期钩子是一个开关,判断是否需要更新,主要用来优化性能。

有一个例外,如果开发者调用this.forceUpdate强制更新,React组件会无视这个钩子。

shouldComponentUpdate生命周期钩子默认返回true。也就是说,默认情况下,只要组件触发了更新,组件就一定会更新。React把判断的控制权给了开发者。

不过周到的React还提供了一个PureComponent基类,它与Component基类的区别是PureComponent自动实现了一个shouldComponentUpdate生命周期钩子。

对于组件来说,只有状态发生改变,才需要重新渲染。所以shouldComponentUpdate生命周期钩子暴露了两个参数,开发者可以通过比较this.propsnextPropsthis.statenextState来判断状态到底有没有发生改变,再相应的返回true或false。

什么情况下状态没改变,却依然触发了更新呢?举个例子:

父组件给子组件传了一个值,当父组件状态变化,即便子组件接收到的值没有变化,子组件也会被迫更新。这显然是非常不合理的,React对此无能为力,只能看开发者的个人造化了。

import React, { Component } from 'react';
import Child from './Child';

class App extends Component {
    state = { name: 'React', star: 1 };

    render() {
        const { name, star } = this.state;
        return (
            <div>
                <Child name={name} />
                <div>{star}</div>
                <button onClick={this.handle}>click</button>
            </div>
        );
    }

	handle = () => {
        this.setState(prevState => ({ star: ++prevState.star }));
    }
}

export default App;
import React, { Component } from 'react';

class Child extends Component {
    render() {
        return <h1>{this.props.name}</h1>;
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (this.props === nextProps) {
            return false;
        } else {
            return true;
        }
    }
}

export default Child;

同时要注意引用类型的坑。

下面这种情况,this.propsnextProps永远不可能相等。

import React, { Component } from 'react';
import Child from './Child';

class App extends Component {
    state = { name: 'React', star: 1 };

    render() {
        return (
            <div>
                <Child name={{ friend: 'Vue' }} />
                <div>{this.state.star}</div>
                <button onClick={this.handle}>click</button>
            </div>
        );
    }

	handle = () => {
        this.setState(prevState => ({ star: ++prevState.star }));
    }
}

export default App;
import React, { Component } from 'react';

class Child extends Component {
    render() {
        return <h1>{this.props.friend}</h1>;
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (this.props === nextProps) {
            return false;
        } else {
            return true;
        }
    }
}

export default Child;

解决方法有两个:

  • 比较this.props.xxxnextProps.xxx
  • 在父组件用一个变量将引用类型缓存起来。

所以this.statenextState是只能用第一种方法比较了,因为React每次更新state都会返回一个新对象,而不是修改原对象。

componentWillUpdate(nextProps, nextState)

💀这是React不再推荐使用的API。

shouldComponentUpdate生命周期钩子返回true,或者调用this.forceUpdate之后,会立即执行该生命周期钩子。

要特别注意,componentWillUpdate生命周期钩子每次更新前都会执行,所以在这里调用this.setState非常危险,有可能会没完没了。

同样,因为Fiber机制的引入,这个生命周期钩子有可能会多次调用。

getSnapshotBeforeUpdate(prevProps, prevState)

👽这是React v16.3.0发布的API。

顾名思义,保存状态快照用的。

它会在组件即将挂载时调用,注意,是即将挂载。它甚至调用的比render还晚,由此可见render并没有完成挂载操作,而是进行构建抽象UI的工作。getSnapshotBeforeUpdate执行完就会立即调用componentDidUpdate生命周期钩子。

它是做什么用的呢?有一些状态,比如网页滚动位置,我不需要它持久化,只需要在组件更新以后能够恢复原来的位置即可。

getSnapshotBeforeUpdate生命周期钩子返回的值会被componentDidUpdate的第三个参数接收,我们可以利用这个通道保存一些不需要持久化的状态,用完即可舍弃。

很显然,它是用来取代componentWillUpdate生命周期钩子的。

意思就是说呀,开发者一般用不到它。

componentDidUpdate(nextProps, nextState, snapshot)

这是组件更新之后触发的生命周期钩子。

搭配getSnapshotBeforeUpdate生命周期钩子使用的时候,第三个参数是getSnapshotBeforeUpdate的返回值。

同样的,componentDidUpdate生命周期钩子每次更新后都会执行,所以在这里调用this.setState也非常危险,有可能会没完没了。

componentWillUnmount()

这是组件卸载之前的生命周期钩子。

为什么组件快要卸载了还需要沉思时刻呢?

因为开发者要擦屁股吖。

React的最佳实践是,组件中用到的事件监听器、订阅器、定时器都要在这里销毁。

当然我说的事件监听器指的是这种:

componentDidMount() {
    document.addEventListener('click', () => {});
}

因为下面这种React会自动销毁,不劳烦开发者了。

render(
	return (
    	<button onClick={this.handle}>click</button>
    );
)

componentDidCatch(error, info)

👽这是React v16.3.0发布的API。

它主要用来捕获错误并进行相应处理,所以它的用法也比较特殊。

定制一个只有componentDidCatch生命周期钩子的ErrorBoundary组件,它只做一件事:如果捕获到错误,则显示错误提示,如果没有捕获到错误,则显示子组件。

将需要捕获错误的组件作为ErrorBoundary的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被ErrorBoundary捕获。

import React, { Component } from 'react';

class ErrorBoundary extends Component {
    state = { hasError: false };

    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
    }
}

export default ErrorBoundary;
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyWidget from './MyWidget';

const App = () => {
    return (
        <ErrorBoundary>
            <MyWidget />
        </ErrorBoundary>
    );
}

export default App;

生命周期

这么多生命周期钩子,实际上总结起来只有三个过程:

  • 挂载
  • 更新
  • 卸载

挂载和卸载只会执行一次,更新会执行多次。

一个完整的React组件生命周期会依次调用如下钩子:

old lifecycle

  • 挂载

    • constructor
    • componentWillMount
    • render
    • componentDidMount
  • 更新

    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  • 卸载

    • componentWillUnmount

new lifecycle

  • 挂载

    • constructor
    • getDerivedStateFromProps
    • render
    • componentDidMount
  • 更新

    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  • 卸载

    • componentWillUnmount

组件树生命周期调用栈

应用初次挂载时,我们以rendercomponentDidMount为例,React首先会调用根组件的render钩子,如果有子组件的话,依次调用子组件的render钩子,调用过程其实就是递归的顺序。

等所有组件的render钩子都递归执行完毕,这时候执行权在最后一个子组件手里,于是开始触发下一轮生命周期钩子,调用最后一个子组件的componentDidMount钩子,然后调用栈依次往上递归。

lifecycle stack

组件树的生命周期调用栈走的是一个Z字形。

如果根组件没有定义A生命周期钩子而子组件定义了,那调用栈就从这个子组件的A生命周期钩子开始。

另外,只要组件内定义了某个生命周期钩子,即便它没有任何动作,也会执行。

app.render();
child.render();
grandson.render();
// divide
grandson.componentDidMount();
child.componentDidMount();
app.componentDidMount();
// divide
app.render();
child.render();
grandson.render();
// divide
grandson.componentDidUpdate();
child.componentDidUpdate();
app.componentDidUpdate();

当然,componentWillMount、componentWillReceiveProps和componentWillUpdate生命周期钩子有可能被打断执行,也有可能被多次调用,表现是不稳定的。所以React决定逐步废弃它们。

不过了解整个应用生命周期的正常调用顺序,还是有助于理解React的。

React专题一览

什么是UI

JSX

可变状态

不可变属性

生命周期

组件

事件

操作DOM

抽象UI