React16-高级教程-六-

97 阅读45分钟

React16 高级教程(六)

原文:Pro React 16

协议:CC BY-NC-SA 4.0

十二、处理事件

在这一章中,我描述了对事件的 React 支持,这些事件是由 HTML 元素生成的,通常是为了响应用户交互。如果您使用过 DOM event API 特性,那么 React 事件特性应该是很熟悉的,但是有一些重要的区别会让粗心的开发人员感到困惑。表 12-1 将 React 事件特性放在上下文中。

表 12-1

将 React 事件放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | React 事件由元素触发以报告重要事件,通常是用户交互。 | | 它们为什么有用? | 事件允许组件响应与它们呈现的内容的交互,这构成了交互式应用的基础。 | | 它们是如何使用的? | 通过向组件呈现的元素添加属性来表示对事件的兴趣。当组件感兴趣的事件被触发时,由属性指定的函数被调用,允许组件更新其状态、调用函数属性或以其他方式反映事件的效果。 | | 有什么陷阱或限制吗? | React 事件类似于 DOM API 提供的事件,但是有一些不同之处,可能会给粗心的人带来隐患,特别是在事件阶段,如“管理事件传播”一节中所述。并非所有由 DOM API 定义的事件都受支持(参见 https://reactjs.org/docs/events.html 获取 React 支持的事件列表)。 | | 有其他选择吗? | 除了使用事件之外,别无选择,事件在用户交互和组件呈现的内容之间提供了必要的链接。 |

表 12-2 总结了本章内容。

表 12-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 处理事件 | 添加与事件名称对应的属性,并使用表达式来处理事件 | 6–10 | | 确定事件类型 | 使用事件对象的type属性 | Eleven | | 防止事件在使用前被重置 | 使用事件对象的persist方法 | 12, 13 | | 用自定义参数调用事件处理程序 | 在 prop 表达式中定义一个内联函数,用所需的数据调用处理程序方法 | 14, 15 | | 阻止事件的默认行为 | 使用事件对象的preventDefault方法 | Sixteen | | 管理事件的传播 | 确定事件阶段 | 17–23 | | 停止一项活动 | 使用事件对象的stopPropagation方法 | Twenty-four |

为本章做准备

为了创建本章的示例项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 12-1 中所示的命令。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npx create-react-app reactevents

Listing 12-1Creating the Example Project

运行清单 12-2 中所示的命令,导航到reactevents文件夹,并将引导包添加到项目中。

cd reactevents
npm install bootstrap@4.1.2

Listing 12-2Adding the Bootstrap CSS Framework

为了在应用中包含引导 CSS 样式表,将清单 12-3 中所示的语句添加到index.js文件中,该文件可以在src文件夹中找到。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 12-3Including Bootstrap in the index.js File in the src Folder

接下来,用清单 12-4 中所示的代码替换App.js文件的内容,这将为本章中的示例提供起点。清单用一个使用类的组件替换了现有的功能组件。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    render() {
        return  (
            <div className="m-2">
                <div className="h4 bg-primary text-white text-center p-2">
                    { this.state.message }
                </div>
                <div className="text-center">
                    <button className="btn btn-primary">Click Me</button>
                </div>
            </div>
        )
    }
}

Listing 12-4The Contents of the App.js File in the src Folder

使用命令提示符,运行reactevents文件夹中清单 12-5 所示的命令来启动开发工具。

npm start

Listing 12-5Starting the Development Tools

一旦项目的初始准备完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000,它将显示如图 12-1 所示的内容。

img/473159_1_En_12_Fig1_HTML.jpg

图 12-1

运行示例应用

了解事件

事件由 HTML 元素触发,以表示重要的更改,例如当用户单击按钮或在文本字段中键入内容时。在 React 中处理事件类似于使用域对象模型 API,尽管有一些重要的区别。在清单 12-6 中,我添加了一个事件处理程序,当点击button元素时会调用它。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    render() {
        return (
            <div className="m-2">
                <div className="h4 bg-primary text-white text-center p-2">
                    { this.state.message }
                </div>
                <div className="text-center">
                    <button className="btn btn-primary"
                        onClick={ () => this.setState({ message: "Clicked!"})}>
                            Click Me
                    </button>
                </div>
            </div>
        )
    }
}

Listing 12-6Adding an Event Handler in the App.js File in the src Folder

使用共享相应 DOM API 属性名称的属性来处理事件,用 camel case 表示。DOM API onclick属性在 React 应用中表示为onClick,并指定如何处理click事件,该事件在用户单击元素时触发。事件处理属性的表达式是一个函数,当指定的事件被触发时将被调用,如下所示:

...
<button className="btn btn-primary"
        onClick={ () => this.setState({ message: "Clicked!"})}>
    Click Me
</button>
...

这是一个内联函数的例子,它调用setState方法来改变message状态数据属性的值。当button元素被点击时,click事件被触发,React 将调用 inline 函数,产生如图 12-2 所示的结果。

img/473159_1_En_12_Fig2_HTML.jpg

图 12-2

处理事件

调用方法来处理事件

有状态组件可以定义方法并使用它们来响应事件,这有助于避免当几个元素以相同的方式处理相同的事件时在表达式中重复代码。对于不改变应用状态或访问其他组件特性的简单方法,可以如清单 12-7 所示指定方法。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    handleEvent() {
        console.log("handleEvent method invoked");
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent }>
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-7Adding an Event Handling Method in the App.js File in the src Folder

注意,onClick表达式不包含括号,这将导致 React 在调用render方法时调用函数,如侧栏中所解释的。handleEvent方法不改变应用的状态,只是向浏览器的 JavaScript 控制台写出一条消息。如果您单击浏览器窗口中的按钮,您将在控制台中看到以下输出:

handleEvent method invoked

避免事件函数调用陷阱

分配给事件处理属性(如onClick)的值必须是一个表达式,该表达式返回 React 可以调用来处理事件的函数。使用事件处理属性时有两个常见错误。第一个错误是用引号而不是大括号将您需要的函数括起来,就像这样:

...
<button className="btn btn-primary" onClick="this.handleEvent" >
...

这为 React 提供了一个字符串值,而不是一个函数,并在浏览器的 JavaScript 控制台中产生一个错误。另一个常见错误是使用调用所需函数的表达式。

...
<button className="btn btn-primary" onClick={ this.handleEvent() } >
...

该表达式导致 React 在创建组件对象时调用handleEvent方法,而不是在触发事件时调用。您不会收到关于此错误的错误或警告,这使得问题更难发现。

在事件处理方法中访问组件功能

如果您需要在处理事件的方法中访问组件的功能,则需要做额外的工作。调用 JavaScript 类方法时,默认情况下不会设置关键字this的值,这意味着handleEvent方法中的语句无法访问组件的方法和属性。在清单 12-8 中,我在handleEvent方法中添加了一个调用setState方法的语句,可以使用this关键字访问该方法。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    handleEvent() {
        this.setState({ message: "Clicked!"});
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent }>
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-8Accessing Component Features in the App.js File in the src Folder

点击button时会调用handleEvent方法,但由于this未定义,会产生以下错误:

Uncaught TypeError: Cannot read property 'setState' of undefined

为了确保给this赋值,可以使用 JavaScript 公共类字段语法来表达事件处理方法,如清单 12-9 所示。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    handleEvent = () => {
        this.setState({ message: "Clicked!"});
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent }>
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-9Redefining an Event Handling Method in the App.js File in the src Folder

方法名后面是等号、左括号和右括号、粗箭头符号,然后是消息体,如清单所示。这是一个笨拙的语法,但我更喜欢它而不是其他的(在侧栏中描述的),这是我在本章和本书其余部分使用的方法。当你点击按钮元素时,handleEvent方法被提供一个this的值,产生如图 12-3 所示的结果。

img/473159_1_En_12_Fig3_HTML.jpg

图 12-3

事件处理程序的绑定

访问组件特征的替代方法

有两种方法可以为事件处理方法提供一个值this。第一种是在事件属性的表达式中使用内联函数。

...
<button className="btn btn-primary"
        onClick={ () => this.handleEvent() }>
    Click Me
</button>
...

请注意,事件处理程序方法是由表达式调用的,这意味着在方法名后面需要左括号和右括号。另一种方法是为组件的每个事件处理程序方法的构造函数添加一条语句。

...
constructor(props) {
    super(props);
    this.state = {
        message: "Ready"
    }
    this.handleEvent = this.handleEvent.bind(this);
}
...

这三种方法都需要一段时间来适应——而且都有点不优雅——你应该遵循你觉得最舒服的方法。

接收事件对象

当事件被触发时,React 向 handler 对象提供一个描述事件的SyntheticEvent对象。SyntheticEvent是由 DOM API 提供的Event对象的包装器,它定义了相同的特性,但是增加了代码以确保事件在不同的浏览器中得到一致的描述。SyntheticEvent对象具有表 12-3 中描述的基本属性和方法。(我将在后面的章节中描述更多的方法和属性。)

表 12-3

由合成事件对象定义的基本属性和方法

|

名字

|

描述

| | --- | --- | | nativeEvent | 该属性返回 DOM API 提供的Event对象。 | | target | 此属性返回表示作为事件源的元素的对象。 | | timeStamp | 该属性返回一个时间戳,指示事件的触发时间。 | | type | 此属性返回一个指示事件类型的字符串。 | | isTrusted | 当浏览器启动事件时,该属性返回true,当代码创建事件对象时,该属性返回false。 | | preventDefault() | 调用此方法是为了防止事件的默认行为,如“防止默认行为”一节所述。 | | defaultPrevented | 如果对事件对象调用了preventDefault方法,该属性返回true,否则返回false。 | | persist() | 调用此方法是为了避免重用事件对象,这对于异步操作很重要,如“避免事件重用陷阱”一节所述。 |

React 事件与 DOM 事件

React 事件在组件和它所呈现的内容之间提供了一个必要的链接——但是 React 事件不是 DOM 事件,即使它们在大多数时间都是相同的。如果您超越了最常用的特性,您将会遇到重要的差异,这些差异可能会产生意想不到的结果。

首先,React 不支持所有事件,这意味着有些 DOM API 事件没有组件可以使用的相应 React 属性。在 https://reactjs.org/docs/events.html 可以看到 React 支持的事件集合。列表中包含了最常用的事件,但并非每个事件都可用。

其次,React 不允许组件创建和发布定制事件。组件间交互的 React 模型是通过函数 props 实现的,如第十章所述,当使用Event.dispatchEvent方法时,自定义事件不会被分发。

第三,React 提供了一个自定义对象作为 DOM 事件对象的包装器,它并不总是以与 DOM 事件相同的方式运行。您可以通过包装器访问 DOM 事件,但是这样做要小心,因为它可能会导致意想不到的副作用。

最后,React 截获处于冒泡阶段的 DOM 事件(将在本章后面描述),并通过组件的层次结构提供它们,为组件提供响应事件和更新它们呈现的内容的机会。这意味着事件包装器对象提供的一些特性不能按预期工作,特别是在传播方面,如“管理事件传播”一节所述。

在清单 12-10 中,我更新了handleEvent方法,以便它使用 React 提供的事件对象来更新组件的状态。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    handleEvent = (event) => {
        this.setState({ message:  `Event: ${event.type} `});
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent }>
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-10Receiving an Event Object in the App.js File in the src Folder

我在handleEvent方法中添加了一个event参数,我用它在显示给用户的消息中包含了type属性的值,如图 12-4 所示。

img/473159_1_En_12_Fig4_HTML.jpg

图 12-4

接收事件对象

区分事件类型

React 在调用事件处理函数时总是提供一个SyntheticEvent对象,如果您习惯于使用instanceof关键字来区分由 DOM API 创建的事件,这可能会引起混淆。在清单 12-11 中,我更改了button元素,因此handleEvent方法用于响应MouseUpMouseDown事件。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready"
        }
    }

    handleEvent = (event) => {
        if (event.type === "mousedown") {
            this.setState({ message: "Down"});
        } else {
            this.setState({ message: "Up"});
        }
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onMouseDown={ this.handleEvent }
                            onMouseUp={ this.handleEvent } >
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-11Differentiating Events in the App.js File in the src Folder

handleEvent方法使用type属性来确定正在处理哪个事件,并相应地更新message值。当你按下鼠标按钮时,触发一个mousedown事件,当你松开鼠标按钮时,触发一个mouseup事件,如图 12-5 所示。

img/473159_1_En_12_Fig5_HTML.jpg

图 12-5

区分事件类型

避免事件重用陷阱

一旦一个事件被处理,React 重用SyntheticEvent对象并将所有属性重置为null。如第十一章所述,如果你依赖异步更新状态数据,这会导致问题。清单 12-12 展示了这个问题。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0
        }
    }

    handleEvent = (event) => {
        this.setState({ counter: this.state.counter + 1},
            () => this.setState({ message: `${event.type}: ${this.state.counter}`}));
    }

    render() {
        return  <div className="m-2">
                    <div className="h4 bg-primary text-white text-center p-2">
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent } >
                                Click Me
                        </button>
                    </div>
            </div>
    }
}

Listing 12-12Using an Event Object Asynchronously in the App.js File in the src Folder

在应用了对counter属性的更新之后,handleEvent方法使用setState方法的回调特性来更新message属性。分配给message属性的值包括事件对象的type属性,这是一个问题,因为当setState回调函数被调用时,该属性将被设置为null,这可以通过单击按钮看到,如图 12-6 所示。

img/473159_1_En_12_Fig6_HTML.jpg

图 12-6

异步使用事件对象

persist方法用于防止 React 重置事件对象,如清单 12-13 所示。

...
handleEvent = (event) => {
    event.persist();
    this.setState({ counter: this.state.counter + 1},
        () => this.setState({ message: `${event.type}: ${this.state.counter}`}));
}
...

Listing 12-13Persisting an Event Object in the App.js File in the src Folder

结果是可以从setState方法的回调函数中读取事件的属性,产生如图 12-7 所示的结果。

img/473159_1_En_12_Fig7_HTML.jpg

图 12-7

持续事件

使用自定义参数调用事件处理程序

如果为事件处理程序提供自定义参数,而不是 React 默认提供的SythenticEvent对象,那么它们通常会更有用。为了演示为什么事件对象并不总是有用,我向由App组件呈现的内容添加了另一个button元素,并设置了事件处理程序,以便它使用该事件来确定哪个按钮被点击了,如清单 12-14 所示。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0,
            theme: "secondary"
        }
    }

    handleEvent = (event) => {
        event.persist();
        this.setState({
            counter: this.state.counter + 1,
            theme: event.target.innerText === "Normal" ? "primary" : "danger"
        }, () => this.setState({ message: `${event.type}: ${this.state.counter}`}));
    }

    render() {
        return  <div className="m-2">
                    <div className={ `h4 bg-${this.state.theme}
                            text-white text-center p-2`}>
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ this.handleEvent } >
                                Normal
                        </button>
                        <button className="btn btn-danger m-1"
                            onClick={ this.handleEvent } >
                                Danger
                        </button>
                    </div>
            </div>
    }
}

Listing 12-14Identifying the Source of an Event in the App.js File in the src Folder

这种方法的问题在于,事件处理程序必须理解组件所呈现内容的重要性。在这种情况下,这意味着知道innerText属性的值可以用来计算事件的来源并确定theme状态数据属性的值。如果组件呈现的内容发生变化,或者如果有多个交互可以产生相同的结果,这可能很难管理。一种更优雅的方法是为事件处理程序属性使用内联表达式,该表达式调用处理程序方法并为其提供所需的信息,如清单 12-15 所示。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0,
            theme: "secondary"
        }
    }

    handleEvent = (event, newTheme) => {
        event.persist();
        this.setState({
            counter: this.state.counter + 1,
            theme: newTheme
        }, () => this.setState({ message: `${event.type}: ${this.state.counter}`}));
    }

    render() {
        return  <div className="m-2">
                    <div className={ `h4 bg-${this.state.theme}
                            text-white text-center p-2`}>
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ (e) => this.handleEvent(e, "primary") } >
                                Normal
                        </button>
                        <button className="btn btn-danger m-1"
                            onClick={ (e) => this.handleEvent(e, "danger") } >
                                Danger
                        </button>
                    </div>
            </div>
    }
}

Listing 12-15Invoking a Handler with a Custom Argument in the App.js File in the src Folder

结果是一样的,但是handleEvent方法不必为了设置theme属性而检查触发事件的元素。要查看设置主题的效果,单击任一按钮元素,如图 12-8 所示。

img/473159_1_En_12_Fig8_HTML.jpg

图 12-8

使用自定义参数

小费

如果你的 handler 方法不需要 event 对象,那么你可以使用 inline 表达式来调用没有它的 handler:() => handleEvent("primary")

防止默认行为

有些事件具有浏览器默认执行的行为。例如,单击复选框的默认行为是切换复选框的状态。可以在事件对象上调用preventDefault方法来防止默认行为,为了进行演示,我在内容中添加了一个checkbox元素,只有在其中一个按钮元素被单击后才会被切换,如清单 12-16 所示。

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0,
            theme: "secondary"
        }
    }

    handleEvent = (event, newTheme) => {
        event.persist();
        this.setState({
            counter: this.state.counter + 1,
            theme: newTheme
        },  () => this.setState({ message: `${event.type}: ${this.state.counter}`}));
    }

    toggleCheckBox = (event) => {
        if (this.state.counter === 0) {
            event.preventDefault();
        }
    }

    render() {
        return  <div className="m-2">
                    <div className="form-check">
                        <input className="form-check-input" type="checkbox"
                             onClick={ this.toggleCheckBox }/>
                        <label>This is a checkbox</label>
                    </div>

                    <div className={ `h4 bg-${this.state.theme}
                            text-white text-center p-2`}>
                        { this.state.message }
                    </div>
                    <div className="text-center">
                        <button className="btn btn-primary"
                            onClick={ (e) => this.handleEvent(e, "primary") } >
                                Normal
                        </button>
                        <button className="btn btn-danger m-1"
                            onClick={ (e) => this.handleEvent(e, "danger") } >
                                Danger
                        </button>
                    </div>
            </div>
    }
}

Listing 12-16Preventing Default Behavior in the App.js File in the src Folder

input元素上的onClick属性告诉 React 在用户单击复选框时调用toggleCheckBox方法。如果counter状态数据属性的值为零,则在事件上调用preventDefault方法,结果是直到点击按钮后才能切换复选框,如图 12-9 所示。

img/473159_1_En_12_Fig9_HTML.jpg

图 12-9

防止事件默认行为

管理事件传播

事件有一个生命周期,它允许元素的祖先接收由它们的后代触发的事件,并且在事件到达元素之前拦截事件。在接下来的章节中,我将描述事件如何通过 HTML 元素传播,并解释这对 React 应用的影响,使用表 12-4 中描述的SyntheticEvent定义的属性和方法。

表 12-4

事件传播的综合事件属性和方法

|

名字

|

描述

| | --- | --- | | eventPhase | 此属性返回事件的传播阶段。但是,React 处理事件的方式意味着该属性没有用,如“确定事件阶段”一节所述。 | | bubbles | 如果事件将进入冒泡阶段,该属性返回true。 | | currentTarget | 此属性返回一个对象,该对象表示其事件处理程序正在处理事件的元素。 | | stopPropagation() | 调用此方法来停止事件传播,如“停止事件传播”一节中所述。 | | isPropagationStopped() | 如果在一个事件上调用了stopPropagation,这个方法返回true。 |

了解目标和气泡阶段

当一个事件第一次被触发时,它进入目标阶段,在这里调用应用于作为事件源的元素的事件处理程序。一旦这些事件处理程序完成,事件就进入冒泡阶段,在这个阶段,事件沿着祖先元素链向上运行,并被用来调用任何已经应用于该类型事件的处理程序。为了帮助演示这些阶段,我在src文件夹中添加了一个名为ThemeButton.js的文件,并用它来定义清单 12-17 中所示的组件。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        this.props.callback(this.props.theme);
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-17The Contents of the ThemeButton.js File in the src Folder

该组件呈现一个包含一个buttonspan元素,并提供一个theme属性,它指定了一个引导 CSS 主题名称,以及一个被调用来选择属性的callback属性。onClick属性已经应用于spanbutton元素。在清单 12-18 中,我更新了App组件以使用ThemeButton组件,并删除了一些在早期示例中使用的代码。

import React, { Component } from 'react';

import { ThemeButton } from "./ThemeButton";

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0,
            theme: "secondary"
        }
    }

    selectTheme = (newTheme) => {
        this.setState({
            theme: newTheme,
            message: `Theme: ${newTheme}`
        });
    }

    render() {
        return  (
            <div className="m-2">
                <div className={ `h4 bg-${this.state.theme}
                        text-white text-center p-2`}>
                    { this.state.message }
                </div>
                <div className="text-center">
                    <ThemeButton theme="primary" callback={ this.selectTheme } />
                    <ThemeButton theme="danger" callback={ this.selectTheme } />
                </div>
            </div>
        )
    }
}

Listing 12-18Applying a Component in the App.js File in the src Folder

单击任一button元素,您将在浏览器的 JavaScript 控制台中看到以下输出:

...
ThemeButton: Type: click Target: BUTTON CurrentTarget: BUTTON
ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN
...

控制台中有两条消息,因为在由ThemeButton组件呈现的内容中有两个onClick属性。第一条消息是在目标阶段生成的,此时事件由触发它的元素的处理程序处理,在本例中是button元素。然后,事件进入冒泡阶段,通过按钮元素的祖先向上传播,并调用任何合适的事件处理程序。在这个例子中,按钮的父元素span也有一个onClick属性,这导致对handleClick方法的两次调用和两条消息被写入控制台。

小费

并非所有类型的事件都有泡沫阶段。根据经验,特定于单个元素的事件——比如获得和失去焦点——不会冒泡。应用于多个元素的事件(例如单击被多个元素占据的屏幕区域)将冒泡。您可以通过读取事件对象的bubbles属性来查看特定事件是否将经历冒泡阶段。

冒泡阶段超出了组件所呈现的内容,并在 HTML 元素的整个层次结构中传播。为了演示,我向由App组件呈现的元素添加了onClick处理程序,当它从由ThemeButton组件呈现的button元素冒泡时,将接收到click事件,如清单 12-19 所示。

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

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            message: "Ready",
            counter: 0,
            theme: "secondary"
        }
    }

    selectTheme = (newTheme) => {
        this.setState({
            theme: newTheme,
            message: `Theme: ${newTheme}`
        });
    }

    handleClick= (event) => {
        console.log(`App: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
    }

    render() {
        return  (
            <div className="m-2" onClick={ this.handleClick }>
                    <div className={ `h4 bg-${this.state.theme}
                            text-white text-center p-2`}>
                        { this.state.message }
                    </div>
                    <div className="text-center" onClick={ this.handleClick }>
                        <ThemeButton theme="primary" callback={ this.selectTheme } />
                        <ThemeButton theme="danger" callback={ this.selectTheme } />
                    </div>
            </div>
        )
    }
}

Listing 12-19Adding Event Handlers in the App.js File in the src Folder

我将onClick属性添加到两个div元素中,当您单击其中一个按钮时,您将看到浏览器的 JavaScript 控制台中显示以下一系列消息(有些浏览器将最后两条消息组合在一起,因为它们是相同的):

...
ThemeButton: Type: click Target: BUTTON CurrentTarget: BUTTON
ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN
App: Type: click Target: BUTTON CurrentTarget: DIV
App: Type: click Target: BUTTON CurrentTarget: DIV
...

SyntheticEvent对象提供了currentTarget属性,该属性返回其事件处理程序被调用的元素,而target属性返回触发事件的元素。

...
console.log(`ThemeButton: Type: ${event.type} `
    + `Target: ${event.target.tagName} `
    + `CurrentTarget: ${event.currentTarget.tagName}`);
...

这些消息显示了click事件在 HTML 元素层次结构中向上传播时的目标和冒泡阶段,如图 12-10 所示。

img/473159_1_En_12_Fig10_HTML.jpg

图 12-10

事件的目标和泡沫阶段

应用组件的事件和元素

事件处理由组件呈现的 HTML 元素执行,不包括用于应用组件的自定义 HTML 元素。例如,向ThemeButton元素添加事件处理程序属性(如onClick)没有任何效果。没有报告错误,但是自定义元素被排除在浏览器显示的 HTML 之外,并且永远不会调用处理程序。

了解捕获阶段

捕获阶段为元素提供了在目标阶段之前处理事件的机会。在捕获阶段,浏览器从body元素开始,沿着与冒泡阶段相反的路径,沿着元素的层次结构向目标前进,并给每个元素处理事件的机会,如图 12-11 所示。

img/473159_1_En_12_Fig11_HTML.jpg

图 12-11

事件捕获阶段

需要一个单独的属性来告诉 React 应该在捕获阶段应用一个事件处理程序,如清单 12-20 所示。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        this.props.callback(this.props.theme);
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }
                        onClickCapture={ this.handleClick }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-20Capturing an Event in the ThemeButton.js File in the src Folder

对于每个事件处理属性,比如onClick,都有一个相应的捕获属性onClickCapture,它在捕获阶段接收事件。在清单中,我将onClickCapture属性应用于span元素,并在表达式中指定了handleClick方法。结果是,span元素将在capturebubble阶段接收到click事件,因为事件沿着 HTML 元素的层次结构向下,然后又向上返回。单击任何一个button元素都会在浏览器的 JavaScript 控制台中产生一条额外的消息。

...

ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN

ThemeButton: Type: click Target: BUTTON CurrentTarget: BUTTON
ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN
App: Type: click Target: BUTTON CurrentTarget: DIV
App: Type: click Target: BUTTON CurrentTarget: DIV
...

确定事件阶段

ThemeButton组件定义的handleClick方法将为每个click事件处理几次事件,并且它从捕获阶段移动到目标阶段,然后是冒泡阶段。每次调用handleClick方法时,它都会调用父组件提供的 function prop,其效果是重复更改App组件的theme state 属性的值。这是一种无害的效果,但是在实际项目中,重复调用回调会导致问题,并且对于子组件来说,假设可以在没有问题的情况下调用 props 是一种不好的做法。为了突出这个问题,我向ThemeButton组件的handleEvent方法添加了一条语句,当调用函数 prop 时,该语句向浏览器的 JavaScript 控制台写入一条消息,如清单 12-21 所示。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        console.log("Invoked function prop");
        this.props.callback(this.props.theme);
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }
                        onClickCapture={ this.handleClick }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-21Adding a Debugging Message in the ThemeButton.js File in the src Folder

单击示例应用提供的一个按钮,您将会看到函数 prop 为click事件经历的三个阶段中的每一个阶段调用。

...
ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN

Invoked function prop

ThemeButton: Type: click Target: BUTTON CurrentTarget: BUTTON

Invoked function prop

ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN

Invoked function prop

App: Type: click Target: BUTTON CurrentTarget: DIV
App: Type: click Target: BUTTON CurrentTarget: DIV
...

React 使用的SythenticEvent对象定义了一个eventPhase属性,该属性从原生 DOM API 事件对象返回相应属性的值。不幸的是,该属性的值总是指示事件处于冒泡阶段,因为 React 截获了本机事件,并使用它来模拟三个传播阶段。因此,需要做更多的工作来识别事件阶段。

第一步是在捕获阶段识别事件,这可以通过使用不同的处理程序方法或向通用处理程序提供额外的参数来完成,这是我在清单 12-22 中采用的方法。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event, capturePhase = false) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        if (capturePhase) {
            console.log("Skipped function prop: capture phase");
        } else {
            console.log("Invoked function prop");
            this.props.callback(this.props.theme);
        }
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }
                        onClickCapture={ (e) => this.handleClick(e, true) }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-22Identifying Capture Phase Events in the ThemeButton.js File in the src Folder

我为接收SythenticEvent对象的onClickCapture属性使用了一个内联表达式,并使用它来调用handleClick方法,以及一个指示事件处于捕获阶段的附加参数。在handleClick方法中,我检查了capturePhase参数的值,以识别捕获阶段的事件。

分离目标和气泡阶段更加困难,因为两个阶段中的事件都由onClick属性处理。确定阶段最可靠的方法是查看targetcurrentTarget属性的值是否不同,以及查看bubbles属性是否为true。如果currentTarget返回的对象不同于target的值,并且事件有一个冒泡阶段,那么有理由假设事件正在冒泡,如清单 12-23 所示。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event, capturePhase = false) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        if (capturePhase) {
            console.log("Skipped function prop: capture phase");
        } else if (event.bubbles && event.currentTarget !== event.target) {
            console.log("Skipped function prop: bubble phase");
        } else {
            console.log("Invoked function prop");
            this.props.callback(this.props.theme);
        }
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }
                        onClickCapture={ (e) => this.handleClick(e, true) }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-23Identifying Bubble Phase Events in the ThemeButton.js File in the src Folder

当您单击一个按钮时,您将在浏览器的 JavaScript 控制台中看到以下消息序列,表明每个阶段都已被识别,并且只在目标阶段调用了函数 prop。

...
ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN

Skipped function prop: capture phase

ThemeButton: Type: click Target: BUTTON CurrentTarget: BUTTON

Invoked function prop

ThemeButton: Type: click Target: BUTTON CurrentTarget: SPAN

Skipped function prop: bubble phase

App: Type: click Target: BUTTON CurrentTarget: DIV
App: Type: click Target: BUTTON CurrentTarget: DIV
...

这些消息还确认了事件阶段的顺序:捕获、瞄准,然后冒泡。

停止事件传播

如果您想要中断正常的传播序列并阻止元素接收事件,了解事件阶段也很重要。在清单 12-24 中,我修改了ThemeButton组件,使其在捕获阶段拦截点击事件并阻止它们到达目标元素。

import React, { Component } from "react";

export class ThemeButton extends Component {

    handleClick = (event, capturePhase = false) => {
        console.log(`ThemeButton: Type: ${event.type} `
            + `Target: ${event.target.tagName} `
            + `CurrentTarget: ${event.currentTarget.tagName}`);
        if (capturePhase) {
            if (this.props.theme === "danger") {
                event.stopPropagation();
                console.log("Stopped event");
            } else {
                console.log("Skipped function prop: capture phase");
            }
        } else if (event.bubbles && event.currentTarget !== event.target) {
            console.log("Skipped function prop: bubble phase");
        } else {
            console.log("Invoked function prop");
            this.props.callback(this.props.theme);
        }
    }

    render() {
        return  <span className="m-1" onClick={ this.handleClick }
                        onClickCapture={ (e) => this.handleClick(e, true) }>
                    <button className={`btn btn-${this.props.theme}`}
                        onClick={ this.handleClick }>
                            Select {this.props.theme } Theme
                    </button>
                </span>
    }
}

Listing 12-24Stopping Event Propagation in the ThemeButton.js File in the src Folder

当在捕获阶段收到一个click事件时,span元素上的onClickCapture属性将调用handleClick方法。当theme属性的值为danger时,调用stopPropagation方法,阻止事件到达button元素,具有阻止用户选择danger主题的效果,如图 12-12 所示。

img/473159_1_En_12_Fig12_HTML.jpg

图 12-12

停止事件

摘要

在本章中,我描述了 React 提供的处理事件的特性。我演示了定义处理函数的不同方法,展示了如何使用事件对象,并展示了如何使用自定义参数。我还解释了 React 事件为什么不同于 DOM API 事件,尽管它们是相似且密切相关的。在本章的最后,我介绍了事件生命周期,并向您展示了事件是如何传播的。在下一章,我将描述组件的生命周期,并解释如何协调状态数据的变化。

十三、协调与生命周期

在这一章中,我将解释 React 如何使用一个叫做协调的过程来有效地处理组件产生的内容。协调过程是 React 为组件提供的更大生命周期的一部分,我将描述不同的生命周期阶段,并向您展示有状态组件如何实现方法以成为主动的生命周期参与者。表 13-1 将协调和组件生命周期放在上下文中。

表 13-1

将协调和生命周期文本置于上下文中

|

问题

|

回答

| | --- | --- | | 这是什么? | 协调是有效处理由组件产生的内容以最小化对文档对象模型(DOM)的更改的过程。协调是应用于有状态组件的更大生命周期的一部分。 | | 为什么有用? | 协调过程有助于应用的性能,而更广泛的组件生命周期为应用开发提供了一致的模型,并为高级项目提供了有用的功能。 | | 如何使用? | 协调过程是自动执行的,不需要任何明确的操作。所有有状态组件都经历相同的生命周期,并且可以通过实现特定的方法(对于基于类的组件)或效果挂钩(对于功能组件)来积极参与。 | | 有什么陷阱或限制吗? | 编写组件时必须小心,使它们适合整个生命周期,包括能够呈现内容,即使它可能不用于更新 DOM。 | | 还有其他选择吗? | 不,生命周期和协调过程是 React 的基本特性。 |

表 13-2 总结了本章内容。

表 13-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 触发协调 | 调用forceUpdate方法 | 15, 16 | | 响应生命周期阶段 | 实现与生命周期阶段相对应的方法 | 17–20 | | 在功能组件中接收通知 | 使用效果挂钩 | 21–23 | | 阻止更新 | 实现shouldComponentUpdate方法 | 24, 25 | | 从 props 设置状态数据 | 实现getDerivedStateFromProps方法 | 26, 27 |

为本章做准备

为了创建本章的示例项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 13-1 中所示的命令。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npx create-react-app lifecycle

Listing 13-1Creating the Example Project

运行清单 13-2 中所示的命令,导航到lifecycle文件夹,并将引导包添加到项目中。

cd lifecycle
npm install bootstrap@4.1.2

Listing 13-2Adding the Bootstrap CSS Framework

为了在应用中包含引导 CSS 样式表,将清单 13-3 中所示的语句添加到index.js文件中,该文件可以在src文件夹中找到。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 13-3Including Bootstrap in the index.js File in the src Folder

创建示例组件

本章中的示例需要一些基本组件。在src文件夹中添加一个名为ActionButton.js的文件,并添加清单 13-4 所示的内容。

import React, { Component } from "react";

export class ActionButton extends Component {

    render() {
        console.log(`Render ActionButton (${this.props.text}) Component `);
        return <button className="btn btn-primary m-2"
                        onClick={ this.props.callback }>
                            { this.props.text }
                </button>
    }
}

Listing 13-4The Contents of the ActionButton.js File in the src Folder

该组件呈现一个按钮,该按钮调用一个函数 prop 来响应click事件。接下来,将名为Message.js的文件添加到src文件夹中,并添加清单 13-5 中所示的内容。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    render() {
        console.log(`Render Message Component `);
        return (
            <div>
                <ActionButton theme="primary"  {...this.props} />
                <div className="h5 text-center p-2">
                    { this.props.message }
                </div>
            </div>
        )
    }
}

Listing 13-5The Contents of the Message.js File in the src Folder

这个组件显示一个作为 prop 接收的消息,并传递一个函数 prop 作为对一个ActionButton的回调,如清单 13-4 中所定义的。接下来,将名为List.js的文件添加到src文件夹中,并添加清单 13-6 中所示的内容。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class List extends Component {

    constructor(props) {
        super(props);
        this.state = {
            names: ["Bob", "Alice", "Dora"]
        }
    }

    reverseList = () => {
        this.setState({ names: this.state.names.reverse()});
    }

    render() {
        console.log("Render List Component");
        return (
            <div>
                <ActionButton callback={ this.reverseList }
                    text="Reverse Names" />
                { this.state.names.map((name, index) => {
                    return <h5 key={ name }>{ name }</h5>
                })}

            </div>
        )
    }
}

Listing 13-6The Contents of the List.js File in the src Folder

这个组件有自己的状态数据,用来呈现一个列表。一个ActionButton组件被提供了一个reverseList方法作为它的函数 prop,它反转了列表中条目的顺序。

最后的修改是用清单 13-7 中所示的代码替换App.js文件的内容,该代码呈现使用其他组件的内容,并定义Message组件所需的状态数据。

import React, { Component } from 'react';
import { Message } from "./Message";
import { List } from "./List";

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        }
    }

    incrementCounter = () => {
        this.setState({ counter: this.state.counter + 1 });
    }

    render() {
        console.log("Render App Component");
        return  <div className="container text-center">
                    <div className="row p-2">
                        <div className="col-6">
                            <Message message={ `Counter: ${this.state.counter}`}
                                callback={ this.incrementCounter }
                                text="Increment Counter" />
                        </div>
                        <div className="col-6">
                            <List />
                        </div>
                    </div>
                </div>

    }
}

Listing 13-7The Contents of the App.js File in the src Folder

App组件呈现的内容使用引导 CSS 网格特性并排显示MessageList组件。counter属性由incrementCounter方法递增,该方法用作Message组件的函数 prop。使用命令提示符,运行lifecycle文件夹中清单 13-8 所示的命令来启动开发工具。

npm start

Listing 13-8Starting the Development Tools

一旦项目的初始准备完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000,它将显示如图 13-1 所示的内容。

img/473159_1_En_13_Fig1_HTML.jpg

图 13-1

运行示例应用

了解内容是如何呈现的

呈现过程的起点是调用ReactDOM.render方法的index.js文件中的语句。

...
ReactDOM.render(<App />, document.getElementById('root'));
...

此方法启动初始呈现过程。React 创建一个新的App组件实例,它由ReactDOM.render方法的第一个参数指定,并调用它的render方法。由App组件呈现的内容包括MessageList元素,React 创建这些组件的实例并调用它们的render方法。流程继续到由MessageList元素呈现的内容中的ActionButton元素,创建ActionButton组件的两个实例,并为每个实例调用render方法。在每个组件上调用render方法的结果是一个 HTML 元素的层次结构,这些元素被插入到由ReactDOM.render方法的第二个参数选择的元素中,创建如图 13-1 所示的内容。初始渲染过程的结果是组件对象和 HTML 元素的层次结构,如图 13-2 所示。

img/473159_1_En_13_Fig2_HTML.jpg

图 13-2

组件及其内容

React 使用浏览器的 API 将 HTML 元素添加到文档对象模型(DOM)中,以便将它们呈现给用户,如图 13-3 所示,并在组件和它们呈现的内容之间创建映射。

img/473159_1_En_13_Fig3_HTML.jpg

图 13-3

将组件映射到它们呈现的内容

浏览器不知道或者不关心组件,它唯一的工作就是在 DOM 中呈现 HTML 元素。React 负责管理组件和处理呈现的内容。

示例应用中的每个组件在其render方法中都有一个console.log语句,浏览器的 JavaScript 控制台中显示的消息显示五个组件对象中的每一个都被要求呈现其内容。

...
Render App Component
Render Message Component
Render ActionButton (Increment Counter) Component
Render List Component
Render ActionButton (Reverse Names) Component
...

有来自一个App组件、一个Message组件、一个List组件和两个ActionButton组件的消息,匹配图 13-2 和 13-3 所示的结构。

了解更新过程

当应用第一次启动时,React 要求所有组件呈现它们的内容,以便可以显示给用户。一旦显示了内容,应用就处于协调的状态,其中显示给用户的内容与组件的状态一致。

当应用被协调时,React 等待某些事情发生变化。在大多数应用中,变化是由用户交互引起的,用户交互触发一个事件并导致对setState方法的调用。setState方法更新组件的状态数据,但是它也将组件标记为“陈旧”,这意味着显示给用户的 HTML 内容可能是过期的。一个事件可能导致多个状态数据更改,一旦它们都被处理,React 将为每个脏组件及其子组件调用render方法。要查看更改的效果,点击浏览器窗口中的增量计数器按钮,如图 13-4 所示。

img/473159_1_En_13_Fig4_HTML.jpg

图 13-4

点击按钮以触发更改

响应click事件的处理程序更新App组件的counter状态数据属性。因为App是顶层组件,这意味着render方法在应用的所有组件上都被调用,这可以在浏览器的 JavaScript 控制台显示的消息中看到。

...
Render App Component
Render Message Component
Render ActionButton (Increment Counter) Component
Render List Component
Render ActionButton (Reverse Names) Component
...

React 只更新受更改影响的组件,最大限度地减少了应用在再次协调之前必须做的工作量。你可以通过点击反转名称按钮来查看这是如何工作的,如图 13-5 所示。

img/473159_1_En_13_Fig5_HTML.jpg

图 13-5

点击一个按钮来触发有限的改变

该按钮的click事件导致List组件的状态数据发生变化,并在浏览器的 JavaScript 控制台中产生以下消息:

...
Render List Component
Render ActionButton (Reverse Names) Component
...

组件List及其子组件ActionButton被标记为陈旧,但是这种变化并没有影响到AppMessage组件或者其他的ActionButton。React 假设这些组件呈现的内容仍然是最新的,不需要更新。

了解调节过程

尽管 React 将调用任何被标记为陈旧的组件的render方法,但它并不总是使用生成的内容。对域对象模型中的 HTML 元素进行更改是一项昂贵的操作,因此 React 将组件返回的内容与先前的结果进行比较,以便它可以要求浏览器执行最少数量的操作,这一过程称为协调

为了演示 React 如何最小化它所做的更改,我对由Message组件呈现的内容做了一个更改,如清单 13-9 所示。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    render() {
        console.log(`Render Message Component `);
        return (
            <div>
                <ActionButton theme="primary"  {...this.props} />
                <div id="messageDiv" className="h5 text-center p-2">
                    { this.props.message }
                </div>
            </div>
        )
    }
}

Listing 13-9Changing Content in the Message.js File in the src Folder

添加了id属性使得操作div元素变得更加容易。使用 F12 开发工具,切换到控制台选项卡,输入清单 13-10 中所示的语句,然后按回车键。所有浏览器都允许执行 JavaScript 任意语句,在 Google Chrome 中,这是通过在控制台选项卡底部的提示中输入代码来实现的。

document.getElementById("messageDiv").classList.add("bg-info")

Listing 13-10Manipulating an HTML Element

该语句使用 DOM API 选择由Message组件呈现的div元素,并将其分配给bg-info类,后者选择由引导 CSS 框架定义的背景颜色。当你点击 Increment Counter 按钮时,div元素的内容被更新,但颜色不变,因为 React 已经将Message组件的render方法返回的内容与之前的结果进行了比较,检测到只有div元素的内容有所不同,如图 13-6 所示。

img/473159_1_En_13_Fig6_HTML.jpg

图 13-6

和解的效果

React 将组件产生的内容与它自己的以前结果的缓存进行比较,这个缓存被称为虚拟 DOM,它是以一种允许有效比较的格式定义的。其效果是,React 不必查询 DOM 中的元素来找出一组更改。

小费

不要混淆专用于 React 的术语虚拟 DOM影子 DOM ,后者是一种最新的浏览器功能,允许内容限定在 HTML 文档的特定部分。

需要第二个示例来确认协调行为,演示 React 如何处理更复杂的更改。在清单 13-11 中,我向Message组件添加了状态数据,并使用它在两种不同的元素类型之间进行切换。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    constructor(props) {
        super(props);
        this.state = {
            showSpan: false
        }
    }

    handleClick = (event) => {
        this.setState({ showSpan: !this.state.showSpan });
        this.props.callback(event);
    }

    getMessageElement() {
        let div = <div id="messageDiv" className="h5 text-center p-2">
                        { this.props.message }
                  </div>
        return this.state.showSpan ? <span>{ div } </span> : div;
    }

    render() {
        console.log(`Render Message Component `);
        return (
            <div>
                <ActionButton theme="primary" {...this.props}
                    callback={ this.handleClick } />
                { this.getMessageElement() }
            </div>
        )
    }
}

Listing 13-11Alternating Elements in the Message.js File in the src Folder

该组件在直接显示一个div元素或将其包装在一个span元素中之间交替。保存更改,并在浏览器的 JavaScript 控制台中执行清单 13-12 中所示的语句来设置div元素的背景颜色。注意,在使用 spread 操作符将 props 传递给ActionButton组件之后,我已经定义了callback属性。Message组件从它的父组件接收一个callback属性,所以我必须在之后定义我的替换来覆盖它。

警告

不要在实际项目中更改组件中的顶级元素,因为这会导致 React 替换 DOM 中的元素,而不执行详细的比较来检测更改。

document.getElementById("messageDiv").classList.add("bg-info")

Listing 13-12Manipulating an HTML Element

当您单击增量计数器按钮时,Message组件的render方法将返回包含span元素的内容。第二次点击按钮,render方法将返回原来的内容,但不显示背景颜色,如图 13-7 所示。

img/473159_1_En_13_Fig7_HTML.jpg

图 13-7

协调不同类型的元素

React 将来自render方法的输出与之前的结果进行比较,并检测span元素的引入。React 并不研究新的span元素的内容来执行更详细的比较,只是用它来替换浏览器正在显示的现有的div元素。

了解列表协调

React 特别支持处理显示数据数组的元素。对列表的大多数操作将大部分元素留在数组中,尽管它们可能经常在不同的位置,比如对对象进行排序时。为了确保 React 能够最大限度地减少显示更改所需的更改次数,从数组生成的元素需要有一个key属性,比如由List组件定义的属性。

...
render() {
    console.log("Render List Component");
    return (
        <div>
            <ActionButton callback={ this.reverseList }
                text="Reverse Names" />
            { this.state.names.map((name, index) => {
                return <h5 key={ name }>{ name }</h5>
            })}
        </div>
    )
}
...

属性的值在元素集合中必须是惟一的,这样 React 才能识别每个元素。为了演示 React 如何最小化更新列表所需的更改,我向由List组件呈现的h5元素添加了一个属性,如清单 13-13 所示。

小费

键值应该是稳定的,这样即使在对数组进行更改的操作之后,它们也应该继续引用同一个对象。一个常见的错误是使用对象在数组中的位置作为其索引,这是不稳定的,因为数组上的许多操作会影响对象的顺序。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class List extends Component {

    constructor(props) {
        super(props);
        this.state = {
            names: ["Bob", "Alice", "Dora"]
        }
    }

    reverseList = () => {
        this.setState({ names: this.state.names.reverse()});
    }

    render() {
        console.log("Render List Component");
        return (
            <div>
                <ActionButton callback={ this.reverseList }
                    text="Reverse Names" />
                { this.state.names.map((name, index) => {
                    return <h5 id={ name.toLowerCase() } key={ name }>{ name }</h5>
                })}
            </div>
        )
    }
}

Listing 13-13Adding an Attribute in the List.js File in the src Folder

添加了id属性使得使用浏览器的 JavaScript 控制台操作元素变得容易,使用的方法与前面的例子相同。使用 JavaScript 控制台执行清单 13-14 中所示的语句,这些语句将h5元素分配给应用引导背景颜色的类。

document.getElementById("bob").classList.add("bg-primary")
document.getElementById("alice").classList.add("bg-secondary")
document.getElementById("dora").classList.add("bg-info")

Listing 13-14Adding Classes to Elements

单击 Reverse Names 按钮,您将看到 h5 元素的顺序发生了变化,但是没有元素被销毁和重新创建,如图 13-8 所示。

img/473159_1_En_13_Fig8_HTML.jpg

图 13-8

对列表中的元素重新排序

明确触发和解

协调过程依赖于 React 通过setState方法得到的更改通知,这允许它确定哪些数据是陈旧的。如果您需要响应应用外部发生的变化,比如外部数据到达时,并不总是可以调用setState方法。对于这些情况,React 提供了forceUpdate方法,该方法可用于显式触发更新,并确保任何更改都反映在呈现给用户的内容中。为了演示显式协调,我在src文件夹中添加了一个名为ExternalCounter.js的文件,并用它来定义清单 13-15 中所示的组件。

警告

如果您发现自己正在使用forceUpdate方法,那么考虑您的应用的设计是值得的。forceUpdate方法是一种钝器,通常可以通过扩展状态数据的使用或应用第十四章中描述的组合技术之一来避免使用。

import React, {Component } from "react";
import { ActionButton } from "./ActionButton";

let externalCounter = 0;

export class ExternalCounter extends Component {

    incrementCounter = () => {
        externalCounter++;
        this.forceUpdate();
    }

    render() {
        return (
            <div>
                <ActionButton callback={ this.incrementCounter }
                    text="External Counter" />
                <div  className="h5 text-center p-2">
                    External: { externalCounter }
                </div>
            </div>
        )
    }
}

Listing 13-15The Contents of the ExternalCounter.js File in the src Folder

对于可以作为状态数据处理的数据来说,这是一个显而易见的候选者,但是并不是所有真实世界的情况都是明确的。在这种情况下,组件依赖于 React 控制之外的变量,这意味着更改变量的值不会将组件标记为状态并启动协调过程。相反,incrementCounter方法调用forceUpdate方法,后者显式地开始协调,并确保新值被合并到显示给用户的内容中。为了将新组件合并到应用中,我对App组件进行了清单 13-16 中所示的更改。

import React, { Component } from 'react';
import { Message } from "./Message";
import { List } from "./List";

import { ExternalCounter } from './ExternalCounter';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        }
    }

    incrementCounter = () => {
        this.setState({ counter: this.state.counter + 1 });
    }

    render() {
        console.log("Render App Component");
        return  <div className="container text-center">
                    <div className="row p-2">
                        <div className="col-4">
                            <Message message={ `Counter: ${this.state.counter}`}
                                callback={ this.incrementCounter }
                                text="Increment Counter" />
                        </div>
                        <div className="col-4">
                            <List />
                        </div>
                        <div className="col-4">
                            <ExternalCounter />
                        </div>
                    </div>
                </div>
    }
}

Listing 13-16Adding a New Component in the App.js File in the src Folder

新组件显示在应用布局的右侧,单击外部计数器按钮会明确地将该组件标记为陈旧,并触发协调过程,如图 13-9 所示。

img/473159_1_En_13_Fig9_HTML.jpg

图 13-9

明确开始协调

了解组件生命周期

大多数基于类的有状态组件实现一个构造函数和render方法。构造函数用于从父节点接收属性并定义状态数据。render方法用于在应用启动和 React 响应更新时生成内容。

构造函数和render方法是更大的组件生命周期的一部分,有状态组件可以通过实现方法来参与其中,这些方法对调用作出 React,以表示生命周期中的变化。在接下来的部分中,我将解释组件生命周期的不同阶段以及每个阶段的方法。为了快速参考,表 13-3 列出了常用的生命周期方法。我在“使用高级生命周期方法”一节中描述了三种高级方法。

表 13-3

有状态组件生命周期方法

|

名字

|

描述

| | --- | --- | | constructor | 当创建组件类的新实例时,调用这个特殊的方法。 | | render | 当 React 需要组件中的内容时,调用此方法。 | | componentDidMount | 此方法在组件呈现的初始内容处理完毕后调用。 | | componentDidUpdate | 在 React 完成更新后的协调过程后,将调用此方法。 | | componentWillUnmount | 在销毁组件之前调用此方法。 | | componentDidCatch | 该方法用于处理错误,如第十四章所述。 |

注意

请参阅“使用效果挂钩”部分,了解挂钩功能如何提供对功能组件的生命周期功能的访问。

了解安装阶段

React 创建一个组件并首次呈现其内容的过程称为挂载,组件实现参与挂载过程的常用方法有三种,如图 13-10 所示。

img/473159_1_En_13_Fig10_HTML.jpg

图 13-10

安装阶段

当 React 需要创建一个组件的新实例时,将调用构造函数,这将使组件有机会从其父组件接收属性,定义其状态数据,并执行其他准备工作。

接下来,调用render方法,以便组件提供将被添加到 DOM 的内容。最后,React 调用componentDidMount方法,告诉组件它的内容已经被添加到 DOM 中。

componentDidMount方法通常用于执行从 web 服务获取数据的 Ajax 请求,我将在第三部分中演示。出于本章的目的,我在Message组件中实现了componentDidMount方法,并用它向浏览器的 JavaScript 控制台写一条消息,如清单 13-17 所示。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    constructor(props) {
        super(props);
        this.state = {
            showSpan: false
        }
    }

    handleClick = (event) => {
        this.setState({ showSpan: !this.state.showSpan });
        this.props.callback(event);
    }

    getMessageElement() {
        let div = <div id="messageDiv" className="h5 text-center p-2">
                        { this.props.message }
                  </div>
        return this.state.showSpan ? <span>{ div } </span> : div;
    }

    render() {
        console.log(`Render Message Component `);
        return (
            <div>
                <ActionButton theme="primary" {...this.props}
                    callback={ this.handleClick } />
                { this.getMessageElement() }
            </div>
        )
    }

    componentDidMount() {
        console.log("componentDidMount Message Component");
    }
}

Listing 13-17Implementing a Lifecycle Method in the Message.js File in the src Folder

保存对Message组件的更改,并在应用更新时检查浏览器的 JavaScript 控制台中显示的消息,您将看到调用了componentDidMount方法。

...
Render App Component
Render Message Component
Render ActionButton (Increment Counter) Component
Render List Component
Render ActionButton (Reverse Names) Component
Render ActionButton (External Counter) Component

componentDidMount Message Component

...

您可以看到,在调用了所有组件的render方法之后,已经调用了componentDidMount方法。当 React 需要组件的新实例时,调用componentDidMount方法,这包括应用启动。但是当 React 在应用运行时创建一个组件实例时,也会发生挂载,比如当内容被有条件地呈现时,如清单 13-18 所示。

import React, { Component } from 'react';
import { Message } from "./Message";
import { List } from "./List";
import { ExternalCounter } from './ExternalCounter';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            showMessage: true
        }
    }

    incrementCounter = () => {
        this.setState({ counter: this.state.counter + 1 });
    }

    handleChange = () => {
        this.setState({ showMessage: !this.state.showMessage });
    }

    render() {
        console.log("Render App Component");
        return  (
            <div className="container text-center">
                <div className="row p-2">
                    <div className="col-4">
                        <div className="form-check">
                            <input type="checkbox" className="form-check-input"
                                checked={ this.state.showMessage }
                                onChange={ this.handleChange } />
                            <label className="form-check-label">Show</label>
                        </div>
                        { this.state.showMessage &&
                            <Message message={ `Counter: ${this.state.counter}`}
                                callback={ this.incrementCounter }
                                text="Increment Counter" />
                        }
                    </div>
                    <div className="col-4">
                        <List />
                    </div>
                    <div className="col-4">
                        <ExternalCounter />
                    </div>
                </div>
            </div>
        )
    }
}

Listing 13-18Conditionally Displaying a Component in the App.js File in the src Folder

我添加了一个复选框,并使用onChange属性注册了handleChange方法来接收change事件,当复选框被切换时就会触发这些事件。该复选框用于控制Message组件的可见性,如图 13-11 所示。

img/473159_1_En_13_Fig11_HTML.jpg

图 13-11

控制组件的可见性

每次复选框被选中时,React 都会创建一个新的Message对象,并遍历挂载过程,依次调用每个方法:constructorrendercomponentDidMount。这可以在浏览器的 JavaScript 控制台中显示的消息中看到。

了解更新阶段

React 响应变更并进行协调的过程被称为更新阶段,它调用render方法从组件获取内容,然后在协调过程完成后调用componentDidUpdate,如图 13-12 所示。

img/473159_1_En_13_Fig12_HTML.jpg

图 13-12

更新阶段

componentDidUpdate方法的主要用途是使用 React refs 特性直接操纵 DOM 中的 HTML 元素,我在第十六章第十六章中对此进行了描述。对于这一章,我已经在Message组件中实现了这个方法,并用它向浏览器的 JavaScript 控制台写一条消息,如清单 13-19 所示。

小费

即使协调过程确定组件生成的内容没有改变,也会调用componentDidUpdate方法。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    // ...other methods omitted for brevity...

    componentDidMount() {
        console.log("componentDidMount Message Component");
    }

    componentDidUpdate() {
        console.log("componentDidUpdate Message Component");
    }
}

Listing 13-19Implementing a Lifecycle Method in the Message.js File in the src Folder

在挂载阶段执行初始呈现之后,一旦 React 完成了协调过程并更新了 DOM,对render方法的任何后续调用都将跟随着对componentDidUpdate方法的调用。单击递增计数器按钮将启动更新阶段,并在浏览器的 JavaScript 控制台中生成以下消息:

...
Render App Component
Render Message Component
Render ActionButton (Increment Counter) Component
Render List Component
Render ActionButton (Reverse Names) Component
Render ActionButton (External Counter) Component

componentDidUpdate Message Component

...

了解卸载阶段

当一个组件即将被销毁时,React 将调用componentWillUnmount方法,该方法为组件提供了释放资源、关闭网络连接和停止任何异步任务的机会。在清单 13-20 中,我在Message组件中实现了componentWillUnmount方法,并使用它向浏览器的 JavaScript 控制台写一条消息。

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    // ...other methods omitted for brevity...

    componentDidMount() {
        console.log("componentDidMount Message Component");
    }

    componentDidUpdate() {
        console.log("componentDidUpdate Message Component");
    }

    componentWillUnmount() {
        console.log("componentWillUnmount Message Component");
    }
}

Listing 13-20Implementing a Lifecycle Method in the Message.js File in the src Folder

您可以通过取消选中我在清单 13-20 中添加的复选框来触发卸载阶段。当 React 协调由App组件呈现的新内容时,它确定不再需要Message组件,并在销毁对象之前调用componentWillUnmount方法,在浏览器的 JavaScript 控制台中产生以下消息:

...
Render App Component
Render List Component
Render ActionButton (Reverse Names) Component
Render ActionButton (External Counter) Component

componentWillUnmount Message Component

...

一旦组件被卸载,React 将不会重用它们。如果需要另一个Message组件,React 将创建一个新对象并执行安装序列,例如当复选框再次切换时。这意味着你总是可以依靠constructorcomponentDidMount方法来初始化一个组件,组件对象永远不会被要求从卸载状态中恢复。

使用效果挂钩

定义为功能的组件不能实现方法,也不能以同样的方式参与生命周期。对于这种类型的组件,钩子特性提供了效果钩子,大致相当于componentDidMountcomponentDidUpdatecomponentWillUnmount方法。为了展示效果挂钩的用法,我在src文件夹中添加了一个名为HooksMessage.js的文件,并添加了清单 13-21 中所示的代码。

import React, { useState, useEffect} from "react";
import { ActionButton } from "./ActionButton";

export function HooksMessage(props) {
    const [showSpan, setShowSpan] = useState(false);

    useEffect(() => console.log("useEffect function invoked"));

    const handleClick = (event) => {
        setShowSpan(!showSpan);
        props.callback(event);
    }

    const getMessageElement = () => {
        let div = <div id="messageDiv" className="h5 text-center p-2">
                        { props.message }
                  </div>
        return showSpan ? <span>{ div } </span> : div;
    }

    return (
        <div>
            <ActionButton theme="primary" {...props} callback={ handleClick } />
            { getMessageElement() }
        </div>
    )
}

Listing 13-21The Contents of the HooksMessage.js File in the src Folder

该组件提供了与Message组件相同的功能,但是被表达为一个使用钩子的函数。useEffect函数用于注册一个函数,该函数将在组件被安装、更新和卸载时被调用。在所有三种情况下都调用相同的函数,这反映了将函数用于组件的本质,而不是类。在清单 13-22 中,我已经将新组件添加到由App组件呈现的内容中。

import React, { Component } from 'react';
import { Message } from "./Message";
import { List } from "./List";
import { ExternalCounter } from './ExternalCounter';

import { HooksMessage } from './HooksMessage';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            showMessage: true
        }
    }

    incrementCounter = () => {
        this.setState({ counter: this.state.counter + 1 });
    }

    handleChange = () => {
        this.setState({ showMessage: !this.state.showMessage });
    }

    render() {
        console.log("Render App Component");
        return  (
            <div className="container text-center">
                <div className="row p-2">
                    <div className="col-4">
                        <div className="form-check">
                            <input type="checkbox" className="form-check-input"
                                checked={ this.state.showMessage }
                                onChange={ this.handleChange } />
                            <label className="form-check-label">Show</label>
                        </div>
                        { this.state.showMessage &&
                            <div>
                                <Message message={ `Counter: ${this.state.counter}`}
                                    callback={ this.incrementCounter }
                                    text="Increment Counter" />
                                <HooksMessage
                                    message={ `Counter: ${this.state.counter}`}
                                    callback={ this.incrementCounter }
                                    text="Increment Counter" />
                            </div>
                        }
                    </div>
                    <div className="col-4">
                        <List />
                    </div>
                    <div className="col-4">
                        <ExternalCounter />
                    </div>
                </div>
            </div>
        )
    }
}

Listing 13-22Rendering a New Component in the App.js File in the src Folder

保存对组件的更改,并检查浏览器的 JavaScript 控制台中显示的消息,以查看组件安装和更新时调用的效果钩子函数,如下所示:

...
Render List Component
ActionButton.js:6 Render ActionButton (Reverse Names) Component
ActionButton.js:6 Render ActionButton (External Counter) Component
Message.js:37 componentDidMount Message Component

HooksMessage.js:7 useEffect function invoked

...

传递给useState的函数可以返回一个清理函数,该函数将在组件被卸载时被调用,提供一个类似于componentWillUnmount方法的特性,如清单 13-23 所示。

import React, { useState, useEffect} from "react";
import { ActionButton } from "./ActionButton";

export function HooksMessage(props) {
    const [showSpan, setShowSpan] = useState(false);

    useEffect(() => {
        console.log("useEffect function invoked")
        return () => console.log("useEffect cleanup");
    });

    const handleClick = (event) => {
        setShowSpan(!showSpan);
        props.callback(event);
    }

    const getMessageElement = () => {
        let div = <div id="messageDiv" className="h5 text-center p-2">
                        { props.message }
                  </div>
        return showSpan ? <span>{ div } </span> : div;
    }

    return (
        <div>
            <ActionButton theme="primary" {...props} callback={ handleClick } />
            { getMessageElement() }
        </div>
    )
}

Listing 13-23Using a Cleanup Function in the HooksMessage.js File in the src Folder

切换复选框以卸载组件,您将在浏览器的 JavaScript 控制台中看到以下消息:

...
Render ActionButton (Reverse Names) Component
ActionButton.js:6 Render ActionButton (External Counter) Component
Message.js:45 componentWillUnmount Message Component

HooksMessage.js:9 useEffect cleanup

...

使用高级生命周期方法

前面几节中描述的特性在许多项目中都很有用,尤其是使用componentDidMount方法请求远程数据,这将在第三部分中演示。React 为基于类的组件提供了高级的生命周期方法,这些方法在我在下面的章节中描述的特定情况下非常有用,尽管其中一个方法是与我在第十六章中描述的 refs 特性结合使用的。为了快速参考,表 13-4 描述了先进的生命周期方法。

表 13-4

高级组件生命周期方法

|

名字

|

描述

| | --- | --- | | shouldComponentUpdate | 此方法允许组件指示它不需要更新。 | | getDerivedStateFromProps | 这个方法允许一个组件根据它收到的属性来设置它的状态数据值。 | | getSnapshotBeforeUpdate | 该方法允许组件在协调过程更新 DOM 之前捕获有关其状态的信息。该方法与第十六章所述的 ref 功能结合使用。 |

防止不必要的组件更新

React 的默认行为是将组件标记为陈旧,并在其状态数据发生变化时呈现其内容。而且,由于组件的状态可以作为属性传递给其子组件,所以子组件也会被呈现,正如您在前面的示例中看到的那样。

组件可以通过实现shouldComponentUpdate方法来覆盖默认行为。这个特性允许组件通过避免在不需要的时候调用render方法来提高应用的性能。

在更新阶段调用shouldComponentUpdate方法,其结果决定 React 是否会调用render方法从组件中获取新鲜内容,如图 13-13 所示。shouldComponentUpdate方法的参数是新的属性和状态对象,可以对它们进行检查并与现有值进行比较。如果shouldComponentUpdate方法返回true,React 将继续更新阶段。如果shouldComponentUpdate方法返回 false,React 将放弃组件的更新阶段,并且不会调用rendercomponentDidUpdate方法。

img/473159_1_En_13_Fig13_HTML.jpg

图 13-13

更新方法的高级序列

在清单 13-24 中,我在Message组件中实现了showComponentUpdate方法,如果消息属性的值没有改变,我用它来防止更新。(为了简洁起见,我还从前面的例子中删除了生命周期方法。)

import React, { Component } from "react";
import { ActionButton } from "./ActionButton";

export class Message extends Component {

    constructor(props) {
        super(props);
        this.state = {
            showSpan: false
        }
    }

    handleClick = (event) => {
        this.setState({ showSpan: !this.state.showSpan });
        this.props.callback(event);
    }

    getMessageElement() {
        let div = <div id="messageDiv" className="h5 text-center p-2">
                        { this.props.message }
                  </div>
        return this.state.showSpan ? <span>{ div } </span> : div;
    }

    render() {
        console.log(`Render Message Component `);
        return (
            <div>
                <ActionButton theme="primary" {...this.props}
                        callback={ this.handleClick } />
                { this.getMessageElement() }
            </div>
        )
    }

    shouldComponentUpdate(newProps, newState) {
        let change = newProps.message !== this.props.message;
        if (change) {
            console.log(`shouldComponentUpdate ${this.props.text}: Update Allowed`)
        } else {
            console.log(`shouldComponentUpdate ${this.props.text}: Update Prevented`)
        }
        return change;
    }
}

Listing 13-24Preventing Updates in the Message.js File in the src Folder

在清单 13-25 中,我修改了App组件,使其呈现两个Message组件,每个组件接收并修改一个状态数据值作为属性。

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

//import { List } from "./List";

//import { ExternalCounter } from './ExternalCounter';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counterLeft: 0,
            counterRight: 0
        }
    }

    incrementCounter = (counter) => {
        if (counter === "left") {
            this.setState({ counterLeft: this.state.counterLeft + 1});
        } else {
            this.setState({ counterRight: this.state.counterRight+ 1});
        }
    }

    render() {
        console.log("Render App Component");
        return (
            <div className="container text-center">
                <div className="row p-2">
                    <div className="col-6">
                        <Message
                            message={ `Left: ${this.state.counterLeft}`}
                            callback={ () => this.incrementCounter("left") }
                            text="Increment Left Counter" />
                    </div>
                    <div className="col-6">
                        <Message
                            message={ `Right: ${this.state.counterRight}`}
                            callback={ () => this.incrementCounter("right") }
                            text="Increment Right Counter" />
                    </div>
                </div>
            </div>
        )
    }
}

Listing 13-25Displaying Side-By-Side Components in the App.js File in the src Folder

App组件渲染的新内容并排显示Message组件,如图 13-14 所示。单击任一按钮元素都会增加该组件的计数器。

img/473159_1_En_13_Fig14_HTML.jpg

图 13-14

并排显示组件

默认的 React 行为是当counterLeftcounterRight状态数据值发生变化时,呈现两个Message组件,这会导致其中一个组件不必要地呈现内容。清单 13-25 中shouldComponentUpdate方法的实现覆盖了这种行为,并确保只有受变更影响的组件被更新。如果您单击应用显示的任何一个按钮,您将在浏览器的 JavaScript 控制台中看到一条消息,指出shouldComponentUpdate阻止了其中一个组件的更新。

...
Render App Component

shouldComponentUpdate Increment Left Counter: Update Allowed

Render Message Component
Render ActionButton (Increment Left Counter) Component

shouldComponentUpdate Increment Right Counter: Update Prevented

...

根据属性值设置状态数据

getDerivedStateFromProps方法在挂载阶段先于render方法调用,在更新阶段先于shouldComponentUpdate方法调用,如图 13-15 所示。getDerivedStateFromProps方法为组件提供了检查属性值的机会,并在呈现其内容之前使用它们来更新其状态数据,旨在供其行为受属性值随时间变化影响的组件使用。

img/473159_1_En_13_Fig15_HTML.jpg

图 13-15

从 props 更新状态数据

getDerivedStateFromProps方法是static,这意味着它不能通过this关键字访问任何实例方法或属性。相反,该方法接收一个包含父组件提供的 props 值的props对象和一个表示当前state数据的state对象。getDerivedStateFromProps方法返回一个新的状态数据对象,它是从属性数据中派生出来的。

为了演示这个方法,我在src文件夹中添加了一个名为DirectionDisplay.js的文件,并用它来定义清单 13-26 中所示的组件。

import React, { Component } from "react";

export class DirectionDisplay extends Component {

    constructor(props) {
        super(props);
        this.state = {
            direction: "up",
            lastValue: 0
        }
    }

    getClasses() {
        return (this.state.direction === "up" ? "bg-success" : "bg-danger")
            + " text-white text-center p-2 m-2";
    }

    render() {
        return <h5 className={ this.getClasses() }>
                    { this.props.value }
                </h5>
    }

    static getDerivedStateFromProps(props, state) {
        if (props.value !== state.lastValue) {
            return {
                lastValue: props.value,
                direction: state.lastValue > props.value ? "down" : "up"
            }
        }
        return state;
    }
}

Listing 13-26The Contents of the DirectionDisplay.js File in the src Folder

该组件显示一个带有背景色的数值,该背景色指示当前值是大于还是小于上一个值。getDerivedStateFromProps方法接收新的属性值和组件的当前状态数据,并使用它们来创建新的状态数据对象,该对象包括属性value已经改变的方向。在清单 13-27 中,我已经更新了App组件,这样它可以呈现DirectionDisplay组件和改变其属性数据值的按钮。

import React, { Component } from 'react';

//import { Message } from "./Message";

import { DirectionDisplay } from './DirectionDisplay';

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 100
        }
    }

    changeCounter = (val) => {
        this.setState({ counter: this.state.counter + val })
    }

    render() {
        console.log("Render App Component");
        return  (
            <div className="container text-center">
                <DirectionDisplay value={ this.state.counter } />
                <div className="text-center">
                    <button className="btn btn-primary m-1"
                        onClick={ () => this.changeCounter(-1)}>Decrease</button>
                    <button className="btn btn-primary m-1"
                        onClick={ () => this.changeCounter(1)}>Increase</button>
                </div>
            </div>
        )
    }
}

Listing 13-27Rendering a New Component in the App.js File in the src Folder

结果是DirectionDisplay组件选择的背景颜色根据getDerivedStateFromProps方法的输出而改变,如图 13-16 所示。

img/473159_1_En_13_Fig16_HTML.jpg

图 13-16

从属性值导出状态数据

小费

注意,只有当属性的值不同时,我才创建一个新的状态数据对象。记住,当祖先的状态改变时,React 将触发组件的更新阶段,这意味着即使组件所依赖的属性值都没有改变,也可以调用getDerivedStateFromProps方法。

摘要

在这一章中,我解释了 React 如何在协调过程中处理组件呈现的内容。我还描述了更广泛的组件生命周期,并向您展示了如何通过实现方法在有状态组件中接收通知。在下一章中,我将描述组合组件来创建复杂功能的不同方式。