网络埋伏纪事

React 初学者教程 6 :传递属性

网络埋伏纪事 · 2016-10-27翻译 · 878阅读 原文链接

处理属性有令人沮丧的一面,在前一个教程中我们已经看到了一点。在只处理一层组件时,将属性从一个组件传递到另一个很简单。但是如果你想将一个属性在多层组件之间传递,事情就开始变得复杂了。

事情变得复杂从来不是一件好事情,所以在本教程中,我们来看看我们怎么做才能让在多层组件之间传递属性变得容易。

问题概述

假如你有一个深度嵌套的组件,并且它的层级看起来像这样子(用很棒的彩色圆建模):

你想做的是,将一个属性,从红色圆一路下来传递到紫色圆。我们做不到的是这种很明显又直观的事情:

你不能直接把一个属性传递到目标组件上。原因与 React 的工作机制有关。React 强制一个命令链,在链中,属性必须从父组件向下流动到直接的子组件。也就是说,在传递一个属性时,你不能跳过子层。这还意味着你的子组件不能把一个属性传回到父组件。所有的通讯是从父到子单向的。

在这些指导方针下,将一个属性从红色圆传到紫色圆看起来有点像这样:

在目标路径上的每个组件不得不从其父组件上接收这个属性,然后重新发送该属性给它的子组件。这个过程会一直重复,直到属性到达最终目标。问题就在这个接收和重新发送的步骤上。

如果你必须将一个称为 color 的属性从代表红色圆的组件,发送到代表紫色圆的组件上,它到目标的路径看起来会是这样的:

现在,假设我们有两个属性需要发送:

如果我们想发送三个属性又该怎么办?或者四个呢?

我们很快会看到这种方法既没有可扩展性,也没有可维护性。对于需要通讯的每个附加的属性,我们将不得不为它添加一个入口作为每个组件声明的一部分。如果在某个时间我们决定重新命名属性,就不得不确保该属性的每个实例也被重新命名。如果删除一个属性,我们需要从依赖该属性的每个组件中删除该属性。总的来说,这是我们在编写代码时,应该努力避免的一种情况。那么我们可以怎么做呢?

详细看看这个问题

在前一节,我们在一个较高层次上讨论这个问题是什么。在我们深入下去推算出解决方案之前,我们需要超越图表,看一个更详细的带有真实代码的示例。我们需要看看如下代码:

var Display = React.createClass({
  render: function() {
    return(
      <div>
        <p>{this.props.color}</p>
        <p>{this.props.num}</p>
        <p>{this.props.size}</p>
      </div>
    );
  }
});

var Label = React.createClass({
  render: function() {
    return (
      <Display color={this.props.color} num={this.props.num} size={this.props.size}/>
    );
  }
});

var Shirt = React.createClass({
  render: function() {
    return (
      <div>
        <Label color={this.props.color} num={this.props.num} size={this.props.size}/>
      </div>
    );
  }
});

ReactDOM.render(
  <div>
    <Shirt color="steelblue" num="3.14" size="medium" />
  </div>,
  document.querySelector("#container")
);

先花几分钟理解一下正在发生什么。

这里我们有一个 Shirt 组件,该组件依赖于 Label 组件的输出,而 Label 组件又依赖于 Display 组件的输出。组件层次看起来是这样子的:

在执行这些代码时,得到的输出没有什么特别的。它只是三行文本:

有趣的部分是文本是如何到这里的。我们看到这三行文本的每一行,都映射到最开始在 ReactDOM.render 内指定的一个属性:

<Shirt color="steelblue" num="3.14" size="medium" />

属性 color、num 和 size(以及它们的值)一路旅行到 Display 组件,这会让即使是最老练的全球旅行者也要妒嫉。让我们跟着这些属性,从它们最初的位置到它们被消费的时候,我确实会认识到这将是对我们已经看到过的知识的一个回顾。如果你发现你自己已经厌烦了,请随便跳到下一节。说了这么多...

当 Shirt 组件用指定的 color、num 和 size 属性调用时,这些属性就开始在 ReactDOM.render 内活起来了:

ReactDOM.render(
  <div>
    <Shirt color="steelblue" num="3.14" size="medium" />
  </div>,
  document.querySelector("#container")
);

我们不仅定义了属性,还用它们要携带的值初始化了。

在 Shirt 组件内,这些属性存储在 props 对象内。要传递这些属性,我们必须显式地从 props 对象访问这些属性,并且将它们作为组件调用的一部分列出。如下是当 Shirt 组件调用 Label 组件时的示例:

var Shirt = React.createClass({
  render: function () {
    return (
      <div>
        <Label color={this.props.color} num={this.props.num} size={this.props.size} />
      </div>
    );
  }
});

注意,color、num 和 size 属性又列出来了。这里与我们在 ReactDOM.render 调用中所看到的唯一的不同之处是,每个属性的值是来自于 props 对象中它们各自的入口,而不是手动输入的。

当 Label 组件上线时,它也有用存储的 color、num 和 size 属性正确填充的 props 对象。你可能看到这里一个模式形成了。如果你需要打一个打哈欠,请随意。

Label 通过重复相同的步骤,并调用 Display 组件,持续着传统:

var Label = React.createClass({
  render: function() {
    return (
      <Display color={this.props.color} num={this.props.num} size={this.props.size}/>
    );
  }
});

Display 组件调用包含了相同的来自于 Label 组件的 props 对象的属性及其值列表。唯一的好消息是到这里我们差不多完成了。Display组件只是显示属性,这些属性是在该组件的 props 对象内填充的。

var Display = React.createClass({
  render: function() {
    return(
      <div>
        <p>{this.props.color}</p>
        <p>{this.props.num}</p>
        <p>{this.props.size}</p>
      </div>
    );
  }
});

唷。所有我们想做的是让 Display 组件显示一些 color、num 和 size 的值。唯一复杂的是,我们想要显示的值最初是作为 ReactDOM.render 的一部分定义的。烦人的解决方案是我们在这里看到的,每个沿着路径到目标的组件都需要访问和重新定义每个属性作为向下传的一部分。这只是可怕。我们可以比这个做得更好,随后你会看到如何做。

遇见扩展运算符

所有这些问题的解决方案在于一个 JavaScript 新概念:扩展运算符(Spread Operator)。如果没有上下文,要解释扩展运算符有点难度,所以这里我们先看一个示例,然后再看扩展运算符的定义。

看看如下的代码片段:

var items = ["1", "2", "3"];

function printStuff(a, b, c) {
  console.log("Printing: " + a + " " + b + " " + c);
}

这里我们有个 items 数组,该数组含有三个值。我们还有一个 printStuff 函数,该函数带有三个参数。我们要做的是将来自 items 数组的三个值指定为 printStuff 函数的参数。听起来很简单,对吧?

如下是一种很常见的实现方式:

printStuff(items[0], items[1], items[2]);

这里我们单独访问每个数组条目,然后将其传递到 printStuff 函数。有了扩展运算符,我们现在可以有更简单的方法。

你完全不需要单独指定数组中的每个条目,只需要像这样做就可以了:

printStuff(...items);

扩展运算符是在 items 数组前的 '…' 字符,使用 '…items' 等于像前面一样分别调用 items[0], item[1], item[2]。printStuff 函数会运行,然后在控制台上打印出数组 1, 2, 3。相当酷,对吧?

现在你已经知道扩展运算符是咋回事了。扩展运算符允许你将数组展开为单个的元素。扩展运算符还会做一些更多的事情,但是现在不重要。我们现在只用它来解决我们的属性传递问题。

正确传递属性

我们刚看到了一个示例,该示例用扩展运算符来避免列举数组中的每个条目作为传递给函数的一部分:

var items = ["1", "2", "3"];

function printStuff(a, b, c) {
  console.log("Printing: " + a + " " + b + " " + c);
}

// 用扩展运算符
printStuff(...items);

// 不用扩展运算符
printStuff(items[0], items[1], items[2]);

我们要面对的在组件之间传递属性这种情况,与分别访问每个数组条目很相似。请容我阐述一下。

在一个组件内,我们的 props 对象看起来像下面这样子:

var props = {
  color: "steelblue",
  num: "3.14",
  size: "medium"
}

作为传递这些属性值到一个子组件的一部分,我们手动从 props 对象访问每个条目:

<Display color={this.props.color} num={this.props.num} size={this.props.size} />

如果有一种方法,能够像用扩展运算符扩展一个数组一样,把对象扩展,并传递属性/值对,是不是很好呢?

事实证明,这里有一种方法。它实际上也用到了扩展运算符。我会在稍后解释如何做,但是这意味着我们可以用 '…props' 调用 Display 组件:

<Display {...props}/>

通过用 '…props',运行时的行为与手动指定 color、run 和 size 属性相同。这意味着我们早期的例子可以简化为如下:

var Display = React.createClass({
  render: function() {
    return(
      <div>
        <p>{this.props.color}</p>
        <p>{this.props.num}</p>
        <p>{this.props.size}</p>
      </div>
    );
  }
});

var Label = React.createClass({
  render: function() {
    return (
      <Display {...this.props}/>
    );
  }
});

var Shirt = React.createClass({
  render: function() {
    return (
      <div>
        <Label {...this.props}/>
      </div>
    );
  }
});

ReactDOM.render(
  <div>
    <Shirt color="steelblue" num="3.14" size="medium" />
  </div>,
  document.querySelector("#container")
);

如果你运行这代码,最终结果与前面的没什么变化。最大的不同之处是我们不再以展开每个属性的形式传递为调用每个组件的一部分。这就解决了我们最初需要解决的所有问题。

通过使用扩展运算符,如果曾经决定要添加属性、重命名属性、删除属性,或者做任何其它与属性相关的鬼把戏,你就不需要做大量不同的修改了。只需要在定义属性的地方做一个修改,在消费该属性的地方做另一个修改。这就够了。所有仅仅传递改属性的中间组件将保持不变,因为 {this.props} 表达式不包含内部到底发生了什么的细节。

总结

ES6/ES2015 委员会设计扩展运算符时,只设计把它用在数组上。它能在对象字面量(例如我们的 props 对象)上使用,归功于 React 扩展了标准。到目前为止,还没有浏览器支持在对象字面量上使用扩展运算符。我们的示例能够运行是因为有 Babel。除了将所有 JSX 转变为浏览器能理解的语言外, Babel 还会将前沿以及实验功能转变成跨浏览器友好的东西。这就是为什么我们能成功在对象字面量上用扩展运算符的原因,也是为什么我们能够优雅地解决在多层组件之间传递属性的问题的原因。

相关文章