React专题:生命周期

栏目: 服务器 · 发布时间: 6年前

内容简介:本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出来我的来我的个人博客 获得无与伦比的阅读体验 生命周期,顾名思义,就是从生到死的过程。

本文是『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()

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

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

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

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

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

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

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

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

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

static getDerivedStateFromProps(props, state)

:alien:这是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)

:skull:这是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)

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

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

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

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

getSnapshotBeforeUpdate(prevProps, prevState)

:alien:这是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)

:alien:这是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 钩子,然后调用栈依次往上递归。

组件树的生命周期调用栈走的是一个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的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Analytics 2.0

Web Analytics 2.0

Avinash Kaushik / Sybex / 2009-10-26 / USD 39.99

The bestselling book Web Analytics: An Hour A Day was the first book in the analytics space to move beyond clickstream analysis. Web Analytics 2.0 will significantly evolve the approaches from the fir......一起来看看 《Web Analytics 2.0》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具