React关键概念——组件与属性

70 阅读16分钟

介绍

在上一章中,您了解了任何基于React的用户界面的关键构建块:组件。您学习了组件的重要性、如何使用组件以及如何自己构建组件。

您还了解了JSX,这是组件函数通常返回的类似HTML的标记。正是这些标记定义了最终网页上应该渲染的内容(换句话说,定义了应该出现在最终提供给访问者的网页上的HTML标记)。

组件能做更多吗?

然而,到目前为止,这些组件的实用性并不高。虽然您可以使用它们将网页内容拆分成更小的构建块,但这些组件的实际可重用性非常有限。例如,您可能会将作为整体课程目标列表一部分的每个课程目标放入其自己的组件中(如果您决定首先将网页内容拆分为多个组件)。

如果您仔细想想,这并不是很有用;如果不同的列表项能够共享一个通用组件,然后通过不同的内容或属性来配置该组件——就像HTML的工作方式那样,效果会更好。

在编写纯HTML代码并描述内容时,您使用可重用的HTML元素,并通过不同的内容或属性来配置它们。例如,您有一个<a> HTML元素,但通过href属性和元素的子内容,您可以构建无数个指向不同资源的不同锚点元素,如下所示:

<a href="https://google.com">Use Google</a>
<a href="https://academind.com">Browse Free Tutorials</a>

这两个元素使用完全相同的HTML元素(<a>),但指向的链接完全不同,会出现在网页上(指向两个完全不同的网站)。

因此,为了充分发挥React组件的潜力,如果您可以像常规HTML元素一样配置它们,那将非常有用。事实证明,您可以做到这一点——通过React的另一个关键概念,叫做props

在组件中使用属性(Props)

如何在组件中使用属性(props)?什么时候需要使用它们?

第二个问题将在稍后更详细地回答。此时,了解通常会有一些可重用的组件,因此需要使用属性,而一些唯一的组件可能不需要属性,已经足够。

问题的“如何”部分是此时更重要的部分,这部分可以分解为两个互补的问题:

  • 将属性传递给组件
  • 在组件中使用属性

将属性传递给组件

如果您从头开始设计React,您希望属性和组件的可配置性如何工作?

当然,会有多种可能的解决方案,但有一个很好的参考模型可以考虑:HTML。如上所述,在使用HTML时,您通过元素标签之间或通过属性传递内容和配置。

幸运的是,React组件在配置方面与HTML元素非常相似。属性(props)只是作为属性传递(给组件)或作为子数据在组件标签之间传递,您还可以混合这两种方法:

<Product id="abc1" price="12.99" />
<FancyLink target="https://some-website.com">Click me</FancyLink>

因此,配置组件是相当直接的——至少,如果从消费者的角度来看(换句话说,看看您如何在JSX中使用它们)。

在组件中使用属性

当编写组件的内部代码时,如何访问传递给组件的属性值?

假设您正在构建一个GoalItem组件,负责输出单个目标项(例如,一个课程目标或项目目标),它将是整体目标列表的一部分。

父组件的JSX标记可能如下所示:

<ul>
  <GoalItem />
  <GoalItem />
  <GoalItem />
</ul>

GoalItem组件内部,目标(没有双关的意思)是接受不同的目标标题,以便可以使用相同的GoalItem组件输出这些不同的标题,作为最终显示给网站访问者的列表的一部分。也许该组件还应该接受另一块数据(例如,内部使用的唯一ID)。

这就是GoalItem组件在JSX中的使用方式,如下所示:

<ul>
  <GoalItem id="g1" title="Finish the book!" />
  <GoalItem id="g2" title="Learn all about React!" />
</ul>

GoalItem组件函数内部,计划可能是像这样输出动态内容(换句话说,输出通过属性传递的数据):

function GoalItem() {
  return <li>{title} (ID: {id})</li>;
}

但这个组件函数将不起作用。它有一个问题:titleid在组件函数内部从未定义过。因此,这段代码会导致错误,因为您正在使用一个未定义的变量。

当然,这些应该不在GoalItem组件内部定义,因为目标是使GoalItem组件可重用,并从外部接收不同的标题和ID值(即,从渲染<GoalItem>组件的组件)。

React为此问题提供了解决方案:一个由React自动传递到每个组件函数的特殊参数值。这是一个包含在JSX代码中设置在组件上的额外配置数据的特殊参数,称为props参数。

前面的组件函数可以(并且应该)重写如下:

function GoalItem(props) {
  return <li>{props.title} (ID: {props.id})</li>;
}

参数的名称(props)由您决定,但使用props作为名称是一种约定,因为这个整体概念被称为props

为了理解这个概念,重要的是要记住,这些组件函数并不是由您在代码中的其他地方调用的,而是由React代为调用。由于React会调用这些函数,它可以在调用时将额外的参数传递给它们。

这个props参数确实是一个额外的参数。React会将它传递给每个组件函数,无论您是否在组件函数定义中将其定义为额外的参数。当然,如果您没有在组件函数中定义props参数,那么在该组件中就无法使用props数据。

这个自动提供的props参数始终包含一个对象(因为React传递一个对象作为该参数的值),该对象的属性就是您在JSX代码中为组件添加的“属性”(例如titleid)。

这就是为什么在这个GoalItem组件示例中,可以通过属性(<GoalItem id="g1" … />)传递自定义数据,并通过props对象及其属性(<li>{props.title}</li>)来消费这些数据的原因。

组件、属性和可重用性

得益于属性(props)这一概念,组件实际上变得可重用了,而不仅仅是理论上可重用的。

如果没有额外的配置,输出三个<GoalItem>组件只能渲染相同的目标三次,因为目标文本(以及任何其他可能需要的数据)必须硬编码到组件函数中。

通过使用上面描述的属性,您可以多次使用相同的组件,且每次都可以配置不同的内容。这使得您可以一次性定义一些通用的标记结构和逻辑(在组件函数中),然后根据需要多次使用它,配置不同的内容。

如果这听起来很熟悉,那确实与常规的JavaScript(或任何其他编程语言)函数的理念完全相同。您定义逻辑一次,然后可以多次调用它,使用不同的输入来获得不同的结果。对于组件而言也是一样——至少在使用属性的概念时是这样的。

特殊的“children”属性

前面提到过,React会自动将props对象传递给组件函数。的确如此,正如所描述的,这个对象包含您在组件中设置的所有属性(在JSX中)作为属性。

但是,React不仅仅将您的属性打包到这个对象中;它还会向props对象添加另一个额外的属性:特殊的children属性(这是一个内置属性,名称是固定的,意味着您不能更改它)。

children属性包含一个非常重要的数据:您可能在组件的开闭标签之间提供的内容。

到目前为止,在上面的示例中,组件大多是自闭合的。<GoalItem id="…" title="…" />在组件标签之间没有任何内容。所有数据都通过属性传递到组件中。

这种方法没有问题。您可以仅通过属性配置组件。但对于某些数据和组件,实际上遵循常规HTML约定,通过组件标签之间传递数据可能会更有意义,也更符合逻辑。而GoalItem组件实际上是一个很好的示例。

哪个方法看起来更直观?

<GoalItem id="g1" title="Learn React" />
<GoalItem id="g1">Learn React</GoalItem>

您可能会觉得第二种方法看起来更直观,更符合常规HTML,因为在HTML中,您也会这样配置一个普通的列表项:<li id="li1">Some list item</li>

当您使用常规HTML元素时,您没有选择(不能仅仅因为想要而为<li>添加一个goal属性),但在使用React和您自己的组件时,您有选择。具体取决于您在组件函数内部如何消费属性。根据内部组件代码,两个方法都可以工作。

不过,您可能希望通过组件标签之间传递某些数据,特殊的children属性允许您这样做。它包含您在组件的开闭标签之间定义的任何内容。因此,在示例2(上面的列表)中,children将包含字符串“Learn React”。

在组件函数中,您可以像处理任何其他属性值一样处理children值:

function GoalItem(props) {
  return <li>{props.children} (ID: {props.id})</li>;
}

哪些组件需要属性?

前面提到过,但这是非常重要的:属性是可选的!

React会始终将属性数据传递到您的组件中,但您不必使用这个props参数。如果您不打算使用它,您甚至不必在组件函数中定义它。

没有硬性规定来定义哪些组件需要属性,哪些不需要。这取决于经验,并且仅取决于组件的角色。

您可能有一个通用的Header组件,显示一个静态的头部(带有logo、标题等),这样的组件可能不需要外部配置(换句话说,不需要传递“属性”或其他数据)。它可以是自包含的,所有必需的值都硬编码到组件中。

但您也会经常构建和使用像GoalItem这样的组件(换句话说,组件需要外部数据才能有用)。每当一个组件在React应用中被多次使用时,它很有可能会使用属性。然而,反过来不一定是对的。虽然您会有一些只用一次的组件,它们不使用属性,但您绝对会有一些整个应用中只使用一次的组件,仍然会利用属性。如前所述,这取决于具体的使用场景和组件。

在本书中,您将看到许多示例和练习,这将帮助您更深入地理解如何构建组件并使用属性。

如何处理多个属性

如前面的示例所示,您并不限于每个组件只有一个属性。实际上,您可以根据组件的需要传递和使用任意多个属性——无论是1个、100个(或更多)属性。

一旦您创建的组件有超过两个或三个属性,可能会出现一个新问题:您是否需要单独添加所有这些属性(换句话说,作为独立的属性),还是可以传递包含分组数据的更少属性,例如数组或对象?

实际上,您可以。React允许将数组和对象作为属性值传递。事实上,任何有效的JavaScript值都可以作为属性值传递!

这使得您可以决定是否要为一个组件使用20个单独的属性(“属性”),或者仅使用一个“大”属性。以下是相同组件以两种不同方式配置的示例:

<Product title="A book" price={29.99} id="p1" />
// 或
const productData = {title: 'A book', price: 29.99, id: 'p1'};
<Product data={productData} />

当然,组件内部也必须进行相应的适配(换句话说,在组件函数中),以期望接收单独或分组的属性。但由于您是开发者,当然,这完全取决于您。

在组件函数内部,您还可以让自己的工作更轻松。

通过props.XYZ访问属性值没有问题,但如果您有一个接收多个属性的组件,重复使用props.XYZ可能会变得繁琐,并使代码变得更难阅读。

您可以使用JavaScript的默认特性来提高可读性:对象解构。

对象解构允许您从一个对象中提取值,并一步到位地将这些值分配给变量或常量:

const user = {name: 'Max', age: 29};
const {name, age} = user; // <-- 对象解构
console.log(name); // 输出 'Max'

因此,您可以使用这种语法来提取所有属性值,并直接在组件函数开始时将它们分配给同名的变量:

function Product({title, price, id}) { // 解构在行动// title, price, id现在作为变量在这个函数内部可用
}

您不必使用这种语法,但它可以让您的工作更轻松。

注意

有关对象解构的更多信息,MDN是一个深入了解的好地方。您可以访问:developer.mozilla.org/en-US/docs/…

展开属性(Spreading Props)

假设您正在构建一个自定义组件,它应该充当其他组件的“包装器”——可能是一个内置组件。

例如,您可以构建一个自定义Link组件,该组件应该返回一个标准的<a>元素,并为其添加一些自定义的样式或逻辑:

function Link({children}) {
  return <a target="_blank" rel="noopener noreferrer">{children}</a>;
};

这个非常简单的示例组件返回一个预配置的<a>元素。这个自定义的Link组件配置了锚点元素,以便新页面始终在新标签页中打开。您可以在React应用中使用这个Link组件来为所有链接提供这种开箱即用的行为。

但是,这个自定义组件有一个问题:它是一个包装核心元素的组件,但通过创建自己的组件,您失去了对核心元素的配置能力。如果您在应用中使用这个Link组件,您将如何设置href属性来配置链接目标?

您可能会尝试以下代码:

<Link href="https://some-site.com">Click here</Link>

然而,这段代码不起作用,因为Link组件并不接受或使用href属性。

当然,您可以调整Link组件函数,以便使用href属性:

function Link({children, href}) {
  return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>;
};

但如果您还希望确保可以在需要时添加download属性呢?

嗯,确实,您可以始终接受更多的属性(并将它们传递给组件内部的<a>元素),但这会减少自定义组件的可重用性和可维护性。

一个更好的解决方案是使用标准的JavaScript展开运算符(即...运算符)和React对该运算符的支持。

例如,以下组件代码是有效的:

function Link({children, config}) {
  return <a {...config} target="_blank" rel="noopener noreferrer">{children}</a>;
};

在这个示例中,config被期望是一个JavaScript对象(即一个键值对的集合)。当在JSX代码中的JSX元素上使用展开运算符(...)时,这个对象会被转换成多个属性。

考虑一下这个config值:

const config = { href: 'https://some-site.com', download: true };

在这种情况下,当在<a>上展开它时(即,<a {...config}>),结果将与您编写以下代码相同:

<a href="https://some-site.com" download={true}>

另一种更常见的模式使用了JavaScript的另一个特性:rest属性。这是一种JavaScript模式,允许您将未解构的属性分组到一个新对象中(该对象只包含这些属性)。

function Link({children, ...props}) {
  return <a {...props} target="_blank" rel="noopener noreferrer">{children}</a>;
};

在这个示例中,在解构props时,只有children属性被解构;其他的属性被存储在一个名为props的新对象中。语法非常类似于展开运算符的语法:您使用三个点(...)。但在这里,您在应该包含所有剩余属性的属性前使用该运算符。因此,使用该运算符的位置决定了它的作用。

然后,您可以像处理任何其他对象一样使用这个rest属性(示例中的props)。在上面的示例中,它再次被用来将其属性作为属性传递给<a>元素。

使用这种模式,您可以更自然地使用Link组件,而不必创建和使用单独的配置对象:

<Link href="https://google.com">Can you google that for me?</Link>

这些行为和模式可以用来构建可重用的组件,同时仍保持它们包装的核心元素的可配置性。这有助于避免长列表的预定义接受属性,并改善组件的可重用性。

属性链(Prop Chains)/ 属性传递(Prop Drilling)

在学习属性时,还有一个值得注意的现象:属性传递或属性链(prop drilling)。

这是每个React开发者在某个时刻都会遇到的问题。当您构建一个稍微复杂一些的React应用时,可能会包含多个嵌套组件层级,这些组件需要互相传递数据。

例如,假设您有一个NavItem组件,它应该输出一个导航链接。在该组件内部,您可能有另一个嵌套组件AnimatedLink,它输出实际的链接(可能带有一些漂亮的动画样式)。

NavItem组件可能如下所示:

function NavItem(props) {
  return <div><AnimatedLink target={props.target} text="Some text" /></div>;
}

AnimatedLink可能定义如下:

function AnimatedLink(props) {
  return <a href={props.target}>{props.text}</a>;
}

在这个例子中,target属性通过NavItem组件传递给AnimatedLink组件。NavItem组件必须接受target属性,因为它必须传递给AnimatedLink

这就是属性传递/属性链的核心:您将一个组件需要的属性从另一个不需要它的组件传递过去。

在应用中进行一些属性传递并不一定是坏事,您完全可以接受它。但如果您最终遇到较长的属性链(换句话说,多个传递组件),您可以使用将在第11章“处理复杂状态”中讨论的解决方案。

总结与关键要点

  • 属性(Props)是React的关键概念,使组件可配置,从而实现可重用。
  • 属性会被React自动收集并传递到组件函数中。
  • 您可以决定(根据每个组件的情况)是否使用属性数据(一个对象)。
  • 属性可以像属性一样传递给组件,或者通过特殊的children属性在开闭标签之间传递。
  • 您可以使用JavaScript特性,如解构、rest属性或展开运算符,编写简洁、灵活的代码。
  • 由于您编写代码,如何通过属性传递数据取决于您。是通过标签之间传递还是作为属性?是单个分组的属性还是多个单值属性?这完全取决于您。