第2章大致介绍了React。我们花了些时间学习了React,了解它的设计和API背后的一些概念,我们甚至还逐步说明了如何用React组件构建一个简单注释框。在第4章中,我们将开始更全面地使用组件并开始构建Letters Social示例项目。但在此之前,我们需要更多地了解如何处理React中的数据并理解数据,是如何在React应用中流动的。这就是本章的内容。

3.1 状态介绍

第2章简要介绍了如何处理React组件中的数据,但如果我们想构建大型的React应用,我们需要花费更多时间来关注它。在本节中,我们将学习:

  • 状态;
  • React如何处理状态;
  • 数据如何通过组件流动。

现代Web应用程序通常构建为数据优先的应用。诚然,仍有许多静态网站(我的博客就是其中之一),但即便是这些网站也是随着时间的推移而不断更新的,而且静态网站通常被认为是与现代Web应用不同类别的网站。人们经常使用的大多数Web应用是高度动态的并且充满了随时间变化的数据。

想想Facebook这样的应用。作为社交网络,数据是所有有用东西的生命线,它提供了与互联网上的其他人进行交互的多种方式,所有这些方法都是通过在浏览器(或者其他平台)上修改和接收数据来实现的。许多其他应用包含要展示在UI中的非常复杂的数据,人们可以理解并容易地使用。开发人员还需要有能力维护和推断这些界面以及数据如何通过界面进行流动,因此应用处理数据的方法与处理随时间变化的数据的能力是一样重要的。我们在下一章开始构建的示例应用Letters Social会使用大量的变化数据,但它不像大多数客户应用或商业应用那样复杂。我们将在本章更明确地阐述它,并在本书的其余部分继续学习如何处理React中的数据。

3.1.1 什么是状态

让我们简单地看看状态,以便当我们查看React中的状态时可以更好地理解它。即使之前从未明确地思考过或听说过程序中的“状态”,但至少可能见过它。大部分程序都可能有一些与它们对应的状态。如果你之前用过诸如Vue、Angular和Ember这样的前端框架,那么几乎可以肯定你一定编写过拥有某种状态的UI。React组件也有状态。那么当谈及“状态”时,我们究竟讨论的是什么?试试下面这个定义。

状态 程序在特定瞬间可访问的所有信息。

这是一个简化的定义,这个定义可能忽略了一些学术上的细微差别,但就我们的目的而言已经足够了。许多学者编写了大量论文致力于精确定义计算机系统中的状态,但对我们而言,状态是程序在一瞬间可访问的信息。这包括,在某个特定时刻无须任何额外的赋值或计算就能够引用的所有值,换句话说,它是瞬间对某个程序的了解的快照。

例如,这可能包括先前创建的任何变量或其他可用的值。当改变一个变量(而不是用它获取值时),程序的状态就会被改变,它与之前不一样了。仅通过读取你就可以检索给定时刻的状态,但当你随着时间的推移而进行了某些修改,程序的状态就会变化。从技术上讲,机器的底层状态在使用时每时每刻都在变化,但我们只关心程序的状态。

让我们看一些代码并逐句检查代码清单3-1中的简单的程序状态。我们不会深入那些发生在幕后的所有底层分配或过程,我们只是尝试更清晰地认识程序中的数据,以便更容易思考React组件。

代码清单3-1 简单的程序状态

const letters = 'Letters';  ⇽--- 在名为letters的变量中保存一个字符串
const splitLetters = letters.split('');  ⇽--- 将letters切分为一个字符串数组
console.log("Let's spell a word!");  ⇽--- 打印出一条消息
splitLetters.forEach(letter => console.log(letter));  ⇽--- 打印出每个字母

代码清单3-1展示了一个简单的脚本,它进行了一些基本赋值和数据操作并将其输出到控制台。这虽然很乏味,但我们可以用它来更多地了解状态。Javascript采用所谓的“运行至完成”(run to completion)语义,这意味着程序将按照通常被认为的顺序从上到下执行。JavaScript引擎常常会以意想不到的方式优化代码,但它仍然以与原始代码一致的方式运行。

尝试从上到下逐行阅读代码清单3-1中的代码。如果想用浏览器的调试器执行它,转至https://codesandbox.io/s/n9mvol5x9p。打开浏览器的开发者工具,逐步执行每行代码并查看所有的变量赋值和其他东西。

考虑到我们的目的,让我们将每行代码当作某个时间点。就状态的简单定义“程序在特定瞬间可访问的所有信息”,如何描述应用在某个时刻的状态?注意,我们让事情简单化了并忽略了闭包、垃圾收集等。

(1)letters是一个变量,将字符串“Letters”赋值给它。

(2)通过从letters拆分出每个字符创建了splitLetters,letters仍旧可用。

(3)来自步骤1和步骤2的所有信息都可用,一个消息被发送到控制台。

(4)程序遍历数组中的每一项并打印输出一个字符。这一过程可能瞬间发生几次,因此对Array.forEach可用的信息对程序也可用。

随着程序向前执行,状态随时间发生变化,更多的信息变得可用,因为任何信息都没有被删除而且引用也没有被改变。表3-1展示了可用信息是如何随着程序向前执行而增加的。

试着走查自己的代码并思考程序的每一行可以用什么信息。我们倾向于简化代码——这里正是这样做的,因为我们不必一次性考虑每个可能的维度——但即便是对于简单的程序也有大量的信息可用。

我们可以认真考虑的一个因素是,当运行的程序变得相当复杂时(就像最简单的UI也倾向于变得复杂),对其的推理认知会变得很困难。我的意思是,系统的复杂性可能很难一下子都记住,而且系统中的逻辑很难让人彻底想清楚。大多数程序就是如此,但涉及UI构建时则尤为困难。

现代浏览器应用的UI常常代表了多种技术的交集,包括服务器提供数据、样式和布局API、JavaScript框架、浏览器API等。UI框架上的进展旨在简化这个问题,但它仍是一个挑战。随着Web应用越来越普及并融入社会和日常生活中,这个挑战往往会随着人们对这些应用的期望变大而增加。如果React是有用的,它需要帮助我们减少或屏蔽一些现代UI的极度复杂的状态。希望诸位会认识到React确实能做到这一点。但如何做的的呢?一种方法是提供两个处理数据的特定API:属性(props)和状态(state)。

3.1.2 可变状态与不可变状态

在React应用中,有两种主要的方法来处理组件中的状态,即通过可以改变的状态和通过不能改变的状态。我们在这里进行了简化:应用中存在多种类型的数据和状态。可以用许多不同的方式表示数据,如二叉树、Map或Set,或者常规JavaScript对象。但与React组件中的状态进行通信和交互的方法归结为这两类,在React中,它们被称为状态(state)(可以在组件中改变的数据)和属性(props)(组件接收并且不应该被组件改变的数据)。

你可能听说过状态和属性被称为可变的与不可变的。这在一定程度上是对的,因为JavaScript并未原生地支持真正的不可变对象(Symbol也许是,但它超出本书的范围了)。在React组件中,状态通常是可变的,而属性不应该被改变。在潜心于React特定的API之前,让我们先稍微深入地探索可变性与不可变性思想。

在第2章中,当状态被称为可变的时,意思是我们可以覆盖或更新该数据(例如可以被覆盖的变量)。另一方面,不可变状态是不能被改变的。还有不可变数据结构,其只能通过受控的方式进行改变(这是React中的状态API的工作方式)。当在第10章和第11章中学习Redux时会模拟不可变数据结构。

我们可以稍微扩展一下可变的和不可变的概念,将相应的数据结构类型包括进来。

  • 不变的——一个不可变的持久数据结构,随着时间的推移可以支持多个版本,但不能直接覆盖;不可变数据结构通常是持久的。
  • 可变的——一个可变的临时数据结构,随着时间的推移只支持一个版本;可变的数据结构在其变化时可以被覆盖并且不支持其他版本。

图3-1展示了这些概念。

图3-1 不可变数据结构与可变数据结构中的持久性和临时性。不可变或持久的数据结构常常记录一段历史并且不会改变,但会对随着时间的推移发生的变化进行版本化。但是,临时数据结构通常不记录历史并且随着每次更新都会被抛弃

另一种考虑不可变数据结构和可变的数据结构之间的区别的方法是考虑这两种数据结构各自拥有的不同能力和内存。临时数据结构只有能力保存一瞬间的数据,而持久数据结构则能够记录数据随时间的变动情况。这正是让不可变数据结构的不可变性变得更加清晰的所在:只制作状态的副本——它们没有被替换。旧状态被新状态替代,但数据却没有被替换。图3-2展示了变化是如何发生的。

图3-2 处理可变数据和不可变数据的变化。临时数据结构没有版本,所以当更改它们时,所有以前的状态都消失了。它们可以说是活在当下,而不可变数据结构能够随时间的推移而持续存在

提示 考虑不可变性与可变性的另一种方法是考虑“保存”和“另存”为之间的区别。许多电脑程序能够保存文件的当前状况或者用不同的名字保存当前文件的副本。不可变数据类似于在保存它时保存了一个副本,而可变数据则能够就地覆盖。

尽管JavaScript本身不支持真正的不可变数据结构,React用可变的方式暴露组件的状态(通过状态进行改变)并将属性作为只读的。通常不可变性和不可变数据结构还有更多知识,但是我们对此的关注无须超过我们对他们已有的了解。如果仍想了解更多,有学术研究在关注这类问题。通过Immutable JS这样的库也能在JavaScript应用中广泛地使用不可变数据结构,但在React中我们只需应对属性API和状态API。

3.2 React中的状态

现在我们已经学习了更多有关状态和(不)可变性的知识。所有这些知识如何纳入React中?好吧,我们在上一章已经了解了一点props API和state API,因此可以料想到它们必定是构建组件方式的重要组成部分。事实上,它们是React组件处理数据和彼此间通信的两种主要方法。

3.2.1 React中的可变状态:组件状态

让我们从状态API开始。虽然我们可以说所有组件都有某种“状态”(一般概念),但并不是React中的所有组件都有本地组件状态。从现在起,当我提到状态(state)时,我是在谈论React的API,而不是一般概念。继承自React.Component类的组件可以访问该API。React会为以此方式创建的组件建立并追踪一个支撑实例。这些组件还可以访问下一章将讨论的一系列生命周期方法。

通过this.state可以访问那些继承自React.Component的组件的状态。在这种情况下,this引用的是类的实例,而state则是一个React会进行追踪的特殊属性。你可能认为只要对state进行赋值或者修改state的属性就能够更新state,但情况并非如此。让我们看看代码清单3-2中一个简单React组件中的组件状态示例。你可以在本地机器上创建这个代码。或者直接,访问https://codesandbox.io/s/ovxpmn340y。

代码清单3-2 使用setState修改组件状态

import React from "react";
import { render } from "react-dom";
class Secret extends React.Component{  ⇽--- 创建一个React组件,随着时间的推移,它会访问持久的组件状态——别忘了将类方法绑定到组件实例上constructor(props) {super(props);this.state = {name: 'top secret!',  ⇽--- 为组件提供一个初始状态以便在render()中尝试访问它时不会返回undefined或抛出错误};this.onButtonClick = this.onButtonClick.bind(this);  ⇽--- 创建一个React组件,随着时间的推移,它会访问持久的组件状态——别忘了将类方法绑定到组件实例上}onButtonClick() {  ⇽--- 初识setState,它是用于修改组件状态的专用API。调用setState时提供一个回调函数,该函数会返回一个新状态对象供React使用this.setState(() => ({name: 'Mark'}));}render() {return (<div><h1>My name is {this.state.name}</h1><button onClick={this.onButtonClick}>reveal the secret!</button>  ⇽--- 将显示名字的函数绑定到由按钮发出的点击事件上</div>)}
}render(<Secret/>,document.getElementById('root')  ⇽--- 将顶层组件渲染到应用最高层的HTML元素中——可以用各种方式确定容器,只要ReactDOM能够找到它
);

代码清单3-2创建了一个简单的组件,当点击按钮时会使用setState更新组件状态从而揭示秘密的名字。注意,在this上可以使用setState,这是因为组件继承了React.Component类。

当点击按钮时,点击事件将被触发,提供给React用于响应事件的函数会被执行。当函数执行时,它会用一个对象作为参数来调用setState方法。这个对象有一个指向字符串的name属性。React会安排更新状态。当更新发生后,React DOM会在需要时更新DOM。render函数会被再次调用,但这一次会有一个不同的值提供给包含this.state.name的JSX表达式语法({})。它会展示“Mark”而不是“top secret!”,我的秘密身份就暴露了!

通常情况下,由于性能和复杂性的影响,开发者想要尽可能谨慎地使用setState(React会为开发者追踪一些东西,而开发者则要在心里追踪另一部分数据)。有些模式在React社区中广受欢迎,它们能够使你几乎不使用组件状态(包括Redux、Mobx、Flux等),这些值得作为应用的可选项进行探索——实际上,我们会在第10章和第11章介绍Redux。尽管通常最好是使用无状态函数组件或者是依赖像Redux这样的模式,但使用setState本身并不是糟糕的做法——它仍然是修改组件中数据的主要API。

继续之前,需要注意绝对不要直接修改React组件中的this.state。如果尝试直接修改this.state,之后调用setState()可能替换掉已做出的改变,更糟糕的是,React并不知道对状态所做的变化。即使可以将组件状态当作可以改变的东西,但仍应该将this.state看作是在组件内不可改变的对象(就像props一样)。

这之所以重要还在于setState()不会立即改变this.state。相反,它创建了一个挂起的状态转换(下一章将更为深入地探讨渲染和变更检测)。因此,调用setState方法后访问this.state可能会返回现有值。因为所有这一切都使得调试情况变得棘手,所以只使用setState()来更改组件状态。

即使是像代码清单3-2中的小交互,也发生了很多事情。我们将在后续章节中继续分解React执行组件更新时所发生的种种步骤,但现在,更仔细地研究组件的render方法则更为重要。请注意,即便执行了状态改变并修改了相关数据,它仍会以一种相对可理解和可预测的方式发生。

尤其美妙的是,开发者可以一次性声明期望的组件外观和结构。没必要为了两个可能存在的不同状态做大量额外工作(展示或不展示高度机密名字)。React处理所有底层的状态绑定和更新过程,开发者只需要说 “名字应该在这里”。React的好处在于它不会强迫你思考每部分状态在每个时刻的情况,就像3.1.1小节所做的那样。

让我们更仔细地了解一下setState API。它是改变React组件中的动态状态的主要方法,并且在应用中经常会使用它。让我们看一下方法签名,了解需要给它传递什么:

setState(updater,[callback]
) -> void

setState接收一个用来设置组件新状态的函数以及一个可选的回调函数。updater函数的签名如下:

(prevState, props) => stateChange

之前版本的React允许传递一个对象而不是函数作为setState的第一个参数。之前版本的React与当前版本的React(16及以上)的一个关键区别在于:传递对象暗示着setState本质上是同步的,而实际发生的情况是React会安排一个对状态的更改。callback格式的签名更好地传递了这个信息并且更符合React的全面声明性异步范式:容许系统(React)安排更新,保证顺序但不保证时间。这与一种更加声明式的UI方法相契合,而且这比用命令式的方式在不同时刻指定数据更新(常常是竞态条件的源泉)更易于思考。

如果需要根据当前状态或属性对状态做一下更新,可以通过prevState和props参数来访问这些状态和属性。当要实现类似Boolean切换的东西或者在执行更新前需要知道上一个值时,这通常很有用。

让我们对setState的机制投注更多的关注。setState会使用updater函数返回的对象与当前状态进行浅合并。这意味着,开发人员可以生成一个对象,而React会将该对象的顶级属性合并到状态中。例如,有一个对象有属性A和属性B,B有一些深层嵌套的属性而A只是一个字符串(’Hi!’)。由于执行的是浅合并,只有顶级属性和它们引用的部分得以保留,而不是B的每个部分。React不会寻找B的深层嵌套属性进行更新。解决这个问题的方法是制作对象的副本,深层更新它,而后使用更新后的对象。也可以用immutable.js这样的库来让处理React的数据结构更容易。

setState是一个用起来很直观的API,为ReactClass组件提供一些需要合并到当前状态中的数据,React会为你把它处理好。如果由于某些原因而需要监听过程的完成情况,可以使用可选的callback函数挂载到该过程。代码清单3-3展示了setState的一个实际的浅合并的例子。像之前一样,使用CodeSandbox可以很容易地创建和运行React组件。这可以省去在自己机器上进行设置的麻烦。

代码清单3-3 使用setState进行浅合并

import React from "react";
import { render } from "react-dom";
class ShallowMerge extends React.Component {constructor(props) {super(props);this.state = {user: {name: 'Mark', //  ⇽--- name存在于初始state的user属性中……colors: {favorite: '',}}};this.onButtonClick = this.onButtonClick.bind(this);}onButtonClick() {this.setState({user: { //  ⇽--- ……但正在设置的state 中并没有name——如果它在上一级的话,浅合并就不会发挥作用了colors: {favorite: 'blue'}}});}render() {return (<div><h1>My favorite color is {this.state.user.colors.favorite} and myname is {this.state.user.name}</h1><button onClick={this.onButtonClick}>show the color!</button></div>)}
}render(<ShallowMerge />,document.getElementById('root')
);

初学React时,忘记浅合并是常见的问题来源。在这个示例中,当点击按钮时,内嵌在初始状态的user键内的name属性会被覆盖,因为新状态中没有它。本来打算保持这两个状态,但一个覆盖了另一个。

练习3-1 思考setState API
本章探讨了React管理组件内状态的组件API,所提及的事情之一就是需要通过seState API来修改状态,而不是直接修改。为什么这是一个问题而且为什么那样做行不通呢?试试https://codesandbox.io/s/j7p824jxnw。

3.2.2 React中的不可变状态:属性

我们已经讨论了React如何通过状态和setState以可变的方式处理数据,但React中的不可变数据怎么样呢?React中,属性(props)是传递不可变数据的主要方式,所有组件都能接收属性(不只是继承自React.Component的组件)并能在其构造函数、render和生命周期方法中使用它们。

React中的属性或多或少是不可变的。使用库和其他工具能够模拟组件中的不可变数据结构,但React的props API本身是半不可变的。如果JavaScript原生的Object.freeze方法可用的话,React会使用它防止添加新属性或移除现有属性。Object.freeze也能防止现有属性(或其可枚举性、可配置性和可写性)被修改并防止原型被修改。这很大程度上防止修改props对象,但它在技术上并不是真正的不可变对象(尽管可以这样想)。

属性是传递给React组件的数据,要么来自父组件要么来自组件自身的defaultProps静态方法。然而组件状态只限于单个组件,属性通常由父组件传递。如果开发人员想:“我是否能用父组件的状态来传递属性给子组件?”,那么你算想到些事了。一个组件的状态可以是另一个组件的属性。

属性通常在JSX中作为属性进行传递,但如果使用React.createElement的话,则可以通过该接口直接将它们传递给子组件。可以将任何有效的JavaScript数据作为一个属性传递给其他组件,甚至可以传递组件(它们毕竟只是类而已)。一旦属性被传递给组件使用,就不能在组件内部改变它们。尽可以尝试,但可能会得到一个像,Uncaught TypeError:Cannot assign to read-only property'<myProperty>'ofobject'#<Object>'这样的错误,或者更糟,React应用将无法如预期一样工作,因为违反了预期的使用方式。

下一节中的代码清单3-4展示了访问属性的方式以及如何不给它们赋值。如前所述,属性可以随时间改变,但不是从组建内部改变。这是单项数据流的一部分,后续章节会涵盖这个主题。简而言之,单向意味着从父组件到子组件一路向下改变数据流。一个使用状态的父组件(继承自React.Component)可以改变自己的状态,这个改变的状态可以作为属性传递给子组件,从而改变属性。

练习3-2 在render方法中调用setState
我们已经明确,setState是更新组件状态的方法。可以在哪里调用setState?我们将在下一章了解组件生命周期的哪个点可以调用setState,但现在让我们先将注意力只放在render方法上。当在组件的render方法中调用setState会发生什么?去https://codesandbox.io/s/48zv2nwqww做些试验。

3.2.3 使用属性:PropTypes和默认属性

当使用属性时,有些API可以在开发过程中提供帮助:PropTypes和默认属性。PropTypes提供了类型检查功能,可以用它指定组件期望接收什么样的属性。可以指定数据类型,甚至可以告诉组件的使用者需要提供什么形式的数据(例如,一个拥有user属性的对象,user属性包含特定的键)。在之前版本的React中,PropTypes是核心React库的一部分,但它现在作为prop-types包单独存在。

prop-types库并非魔法——它是一组能够帮助你对输入进行类型检查的函数和属性——可以在其他库中很容易地用它来对输入进行类型检查。例如,可以将prop-types引入另一个类似于React的组件驱动框架(如Preact)并用相似的方式使用它。

要为组件设置PropTypes,需要在类上提供一个叫作propTypes的静态属性。注意代码清单3-4,在组件类上设置的静态属性的名字是以小写字母开头的,而从prop-types库访问的对象名是以大写字母开头的(PropTypes)。为了指定组件需要哪个属性,需要添加要验证的属性名并为其分配一个来自prop-types库默认导出(import PropTypes from 'prop-types')的属性。使用PropTypes可以为属性声明任何类型、形式和必要性(可选还是强制)。

另一个可以让开发体验更为简单的工具是默认属性。还记得如何使用类的构造方法(constructor)为组件提供初始状态?也可以为属性做类似的事情。你可以通过一个名为defaultProps的静态属性来为组件提供默认属性。使用默认属性可以帮助确保组件拥有运行所需的东西,即便使用组件的人忘记为其提供属性。代码清单3-4展示了在组件中使用PropTypes和默认属性的例子。你可以前往https://codesandbox.io/ s/31ml5pmk4m运行代码。

代码清单3-4 React组件的不可变属性

import React from "react";
import { render } from "react-dom";
import PropTypes from "prop-types";class Counter extends React.Component {static propTypes = {  ⇽--- 指定一个描述“形式”的对象incrementBy: PropTypes.number,onIncrement: PropTypes.func.isRequired  ⇽--- 可以为任何propTypes链接isRequired从而确保在属性没有出现时展示警告};static defaultProps = {incrementBy: 1};constructor(props) {super(props);this.state = {count: 0};this.onButtonClick = this.onButtonClick.bind(this);}onButtonClick() {this.setState(function(prevState, props) {return { count: prevState.count + props.incrementBy };});}render() {return (<div><h1>{this.state.count}</h1><button onClick={this.onButtonClick}>++</button></div>);}
}render(<Counter incrementBy={1} />, document.getElementById("root"));

3.2.4 无状态函数组件

要做些什么才能创建只使用属性并且没有状态的简单组件?这是通常的使用情况,特别是我们之后将探索的一些常见的对React友好的应用架构模式,如Flux和Redux。在这些情况下,我们通常希望将状态保存在一个中心位置而不是分散保存到组件中。但其他情形中只使用属性也是有用的。如果React不必管理支撑实例,那么减少应用在资源使用上的损耗也是不错的。

事实证明,可以创建一种只使用属性的组件:无状态函数组件。这些组件有时被开发人员称为无状态组件、函数组件和其他类似的名字,这一点有时让人很难了解正在讨论的内容。它们通常指的是同一件事——一个没有继承React.Component的组件,因此不能访问组件状态或其他生命周期方法。

不足为奇,无状态功能组件只是不能访问或使用React的状态API(或继承自React.Component的其他方法)的组件。它之所以没有状态并不是因为它没有任何种类的(一般)状态,而是因为它不会获得React进行管理的支撑实例。这意味着没有生命周期方法(第4章会涵盖),没有组件状态,并且可能占用更少的内存。

无状态函数组件是函数,因为它们可以被编写为命名函数或是赋值给变量的匿名函数表达式。它们只接收属性,而且对于给定的输入它们会返回相同的输出,因此基本上被认为是纯函数。这使得它们很快,因为React有可能通过避免不必要的生命周期检查或内存分配来进行优化。代码清单3-5展示了无状态函数组件的例子。你可以前往https://codesandbox.io/s/l756002969运行代码。

代码清单3-5 无状态函数组件

import React from "react";
import { render } from "react-dom";
import PropTypes from "prop-types";
function Greeting(props) {  ⇽--- 可以使用函数或匿名函数创建无状态函数组件return <div>Hello {props.for}!</div>;
}
Greeting.propTypes = {for: PropTypes.string.isRequired  ⇽--- 对于任何形式的无状态函数组件,可以用函数或变量的属性来指定propTypes和默认属性
};
Greeting.defaultProps = {for: 'friend'
};  ⇽--- 对于任何形式的无状态函数组件,可以用函数或变量的属性来指定propTypes和默认属性render(<Greeting for="Mark" />, mountNode);// 或者使用箭头函数
// const Greeting = (props) => <div>Hello {props.for}</div>;
// 像之前那样指定PropTypes和默认属性
// render(<Greeting name="Mark" />, document.getElementById("root"));  ⇽--- 可以使用函数或匿名函数创建无状态函数组件

无状态函数组件很强大,特别是与拥有支撑实例的父组件结合使用时。与其在多个组件间设置状态,不如创建单个有状态的父组件并让其余部分使用轻量级子组件。第10章和第11章中,我们会使用Redux将这个模式提升到全新的水平。使用Redux的React应用常常会创建更少的有状态组件(尽管仍有让有状态组件发挥作用的情况)并将状态集中到单个位置中(就是指store)。

练习3-3 使用一个组件的状态来修改另一个组件的属性
本章讨论了属性和状态这两种处理和传递React组件中数据的主要方法。绝对不要直接修改状态或属性,而是使用setState告诉React更新组件的状态。如何使用一个组件的状态来修改另一个组件的属性?你可以前往https://codesandbox.io/s/38zq71q75进行尝试。

3.3 组件通信

当构建简单的评论框组件时,我们已经看到能够用其他组件创建组件。这是React特别棒的原因之一。开发人员能够轻易地用子组件构建其他组件,与此同时还能够保持事物良好地捆绑在一起,并且还能很容易地表示组件间的is-a和has-a关系。这意味着可以将组件看作组件的一部分或是一种特定的东西。

能够混合和匹配组件并灵活地构建东西是很棒的,但如何让它们彼此通信呢?许多框架和库提供了框架特有的方法让应用的不同部分彼此通信。Angular.js或Ember.js中,你可能听说过或曾经使用服务在应用的不同部分之间进行通信。通常这些是广泛可用的长期对象,开发人员可以在其中存储状态并从应用的不同部分进行访问。

React使用了服务或类似的东西吗?没有。在React中,如果想让组件彼此通信,需要传递属性,并且当传递属性时,开发人员做了两件简单的事情:

  • 访问父组件中的数据(要么是状态要么是属性);
  • 传递数据给子组件。

代码清单3-6的示例既展示了你熟悉的父子关系,也展示了所属关系。你可以前往https://codesandbox.io/s/ pm18mlz8jm运行代码。

代码清单3-6 从父组件向子组件传递属性

import React from "react";
import { render } from "react-dom";
import PropTypes from "prop-types";
const UserProfile = props => {  ⇽--- 创建一个返回示例图片的无状态函数组件return <img src={`https://*******. *******.com/user/${props.username}`} />;
};
UserProfile.propTypes = {pagename: PropTypes.string  ⇽--- 记住,即使是在无状态函数组件上,仍可以指定默认属性和propTypes
};UserProfile.defaultProps = {pagename: "erondu"  ⇽--- 记住,即使是在无状态函数组件上,仍可以指定默认属性和propTypes
};const UserProfileLink = props => {return <a href={`https://ifelse.io/${props.username}`}>{props.username}</a>;
};const UserCard = props => {  ⇽--- UserCard是UserProfile和UserProfileLink的父组件return (<div><UserProfile username={props.username} /><UserProfileLink username={props.username} /></div>);
};render(<UserCard username="erondu" />, document.getElementById("root"));

3.4 单向数据流

如果之前使用框架开发过Web应用,可能已经熟悉术语双向数据绑定(two-way data binding)。数据绑定是建立应用UI与其他数据之间联系的过程。在实践中,这常常表现为连接模型这样的应用数据(如用户)和用户界面的库或框架并会保持两者同步。它们彼此同步因此被绑定在一起。React中一个更有帮助的思考方法是将其作为投影:UI是投射到视图中的数据,当数据变化时,视图随之变化,如图3-3所示。

图3-3 数据绑定通常指的是在应用数据与视图(该数据的展示)之间建立连接的过程。另一种思考方式是将其作为数据向用户能够看到的东西(如视图)的投射

数据流是另一种思考数据绑定的方法:数据如何流经应用的不同部分?本质上,人们会问:“什么能够更新什么,从哪里更新,以及如何更新?”如果想用好工具,那么理解正在使用的工具如何塑造、维护和移动数据是无比重要的。不同的库和框架会采用不同的数据流方法(React对如何处理数据流并没有不同的想法)。

React中,数据流是单向的。这意味着实体间的流动并非水平的——这种情况下彼此可以相互更新,而是建立了一个层次结构。可以通过组件传递数据,但如果不传递属性,就不能触及和修改其他组件的状态或属性,也无法修改父组件中的数据。

但可以通过回调函数将数据传回层次结构的上层。当父组件接收到来自子组件的回调函数时,它可以修改其数据并将修改的数据传递给子组件。即便是对于有回调函数的情况,数据总体上仍是向下流动的并仍由向下传递该数据的父组件决定。这就是为什么我们称React中的数据流是单向的,如图3-4所示。

图3-4 数据在React中是按一个方向流动的。属性由父组件传递给子组件(从所有者到拥有者),并且子组件不能编辑父组件的状态或属性。每个拥有支撑实例的组件都能修改它自己的状态但无法修改超出其自身的东西,除了设置其子组件的属性

单向数据流在构建UI时特别有用,因为它让思考数据在应用中流动的方式变得更简单。得益于组件的层次结构以及将属性与状态局限于组件的方式,预测数据如何在应用中移动通常更容易。

某种程度上避免这个层次结构听上去似乎不错,可以而且从应用的任何部分随意修改想要修改的东西,但实际上这往往会导致难以琢磨的应用并且可能造成困难的调试情况。后续章节将探索Flux和Redux这样的架构模式,它允许维护单向数据流范式的同时协调跨组件或跨应用的行动。

3.5 小结

本章讨论了如下主题。

  • 状态是程序在特定瞬间可访问的信息。
  • 不可变状态不会改变,而可变状态会改变。
  • 持久的、不可变的数据结构不会改变——它们只记录其改变并创建自己的副本。
  • 临时的、可变的数据结构会在更新时被清除。
  • React即使用可变数据(组件本地状态),也使用伪不可变数据(属性)。
  • 属性是伪不可变的并且一旦设置就不应该被修改。
  • 组件状态由支撑实例追踪并且可以使用setState进行修改。
  • setState执行数据的浅合并、更新组件状态,保留任何没有被覆盖的顶级属性。
  • React中的数据流是单向的,从父组件流向子组件。子组件通过回调函数将数据回送给父组件,但它们不能直接修改父组件的状态,而且父组件也无法直接修改子组件的状态。组件通过属性完成组件交互。

本文摘自《React实战》

React的设计初衷就是,帮助开发者为用户提供令人惊叹的用户体验。每位开发者都可以使用React这个强大的工具!管理状态、数据流和渲染的巧妙设计是成功的关键,只有这样设计的应用才能运行顺畅、让人记忆犹新。开发者只要进入这个由组件和库构成的极其丰富的生态系统,就可以掌握构建让开发者和用户都赏心悦目的Web应用的秘诀。
本书指导读者像专家一样思考用户界面(UI),并教读者用React构建它们。本书非常实用,配有很多可实际操作的示例,让读者快速上手。本书的目标是让读者掌握渲染、生命周期方法、JSX、数据流、表单、路由、与第三方库集成和测试等核心概念,并且帮助读者利用书中介绍的应用设计理念推动应用的流行。在学习将React集成到全栈应用的过程中,读者还可以探索通过Redu**行状态管理和服务器端渲染,甚至可以接触到用于移动UI 的React Native。

如何将某个groupbox中的数据赋值到另一个groupbox_React中的数据和数据流相关推荐

  1. 萌新的Python练习实例100例(七)将一个列表的数据复制到另一个列表中

    题目: 将一个列表的数据复制到另一个列表中 分析: · 这道题是联系list和切片的使用: 方法1: · 将b的值赋予a: · a[0:3]表示使用切片从0位置开始到第3位置结束: · a[:3]表示 ...

  2. Python-将一个列表的数据复制到另一个列表中

    # 题目:将一个列表的数据复制到另一个列表中 list1 = [x for x in range(1,5)]list2 = []print(list1)print(list2) #初始化 print( ...

  3. MySql中把一个表的数据插入到另一个表中的实现代码--转

    MySql中把一个表的数据插入到另一个表中的实现代码 司的一个项目,做报表--要关联的表结构比较多,最后决定把要用的数据集合到一张新表中,需要用到以下的sql语法......分享下: web开发中,我 ...

  4. oracle把一个表的数据复制到另一个表中

    1. 新增一个表,通过另一个表的结构和数据 create table XTHAME.tab1 as select * from DSKNOW.COMBDVERSION 2. 如果表存在: insert ...

  5. python excel 数据匹配_python将一个excel表格的数据匹配到另一个表中

    python将一个excel表格的数据匹配到另一个表中 python将一个excel表格的数据匹配到另一个表中 打开excel表,需要在另一个表中匹配相应学生姓名的学号信息. 之前尝试了excel中的 ...

  6. 如何在另一个表格进行计算机,如何将一个excel表格的数据匹配到另一个表中

    日常生活办公,计算机二级考试需要用到Excel,能够熟练使用Excel可以为自己处理数据表格带来极大的帮助,本文就"如何将一个excel表格的数据匹配到另一个表中"的方法进行一个详 ...

  7. 将一个excel表格的数据匹配到另一个表中

    将一个excel表格的数据匹配到另一个表中 第一步:我们打开一个excel表,需要在另一个表中匹配出相应学校的信息. 第二步:我们把光标定位在需要展示数据的单元格中,在单元格中输入"=vl& ...

  8. VLOOKUP函数将一个excel表格的数据匹配到另一个表中

    sklearn实战-乳腺癌细胞数据挖掘(博主亲自录制视频) https://study.163.com/course/introduction.htm?courseId=1005269003& ...

  9. 如何将一个excel表格的数据匹配到另一个表中

    我们在操作excel表的时,有时需要将一个excel表中的数据匹配到另一个表中,那么就需要用到VLOOKUP函数,VLOOKUP函数是Excel中的一个纵向查找函数,VLOOKUP是按列查找,最终返回 ...

最新文章

  1. LTE: MIB和SIB,小区选择和重选规则
  2. Gitlab Issue Tracker and Wiki(一)
  3. 第八周项目一-数组做数据成员(3)
  4. 读《程序是怎样跑起来》第五章有感
  5. PHP EOF(heredoc)的使用方法
  6. 模块调用时对参数值的更改
  7. linux 安装apache、tomcat问题汇总
  8. 【分享】时至今日,深度学习领域有哪些值得追踪的前沿研究?
  9. python fileinput处理多文件
  10. Spark出租车数据实验实用说明书
  11. python如何运行js代码
  12. MATLAB运行cpp文件(从配置到运行)
  13. DailyTopic_4/16 B站:最有YouTube气质的视频网站
  14. 现实感:找准定位,躬身前行
  15. 用Java实现网易云音乐爬虫(非selenium)
  16. 八字易经算法之用JAVA实现日子吉凶星
  17. Autoware.Auto
  18. 移植ucos-III到stm32f103c8t6
  19. web实例之电子日历
  20. Instruction Set Principles

热门文章

  1. [转]DICOM医学图像处理:Deconstructed PACS之Orthanc
  2. shopnc 发票项目
  3. 数据结构树之二分查找树
  4. Java web程序中备份oracle数据库
  5. 数学在机器学习中的重要性[by Dahua Lin]
  6. DWORD dwSendTime =::GetTickCount();
  7. 西南石油大学计算机二级成绩查询,西南石油大学本科学生成绩自助查询打印终端管理办法...
  8. python中insert()函数的用法_Python list insert()用法及代码示例
  9. linux查询关键词上下行_【已解决】Linux下通过关键字模糊查找搜索文件
  10. python 钉钉机器人自定义发送文件_Python自定义钉钉机器人发送自动化结果报告...