深入浅出React和Redux
上QQ阅读APP看书,第一时间看更新

2.2.1 React的prop

在React中,prop(property的简写)是从外部传递给组件的数据,一个React组件通过定义自己能够接受的prop就定义了自己的对外公共接口。

每个React组件都是独立存在的模块,组件之外的一切都是外部世界,外部世界就是通过prop来和组件对话的。

1.给prop赋值

我们先从外部世界来看,prop是如何使用的,在下面的JSX代码片段中,就使用了prop:

<SampleButton
  id="sample" borderWidth={2} onClick={onButtonClick}
  style={{color: "red"}}
/>

在上面的例子中,创建了名为SampleButton的组件实例,使用了名字分别为id、border Width、onClick和style的prop,看起来,React组件的prop很像是HTML元素的属性,不过,HTML组件属性的值都是字符串类型,即使是内嵌JavaScript,也依然是字符串形式表示代码。React组件的prop所能支持的类型则丰富得多,除了字符串,可以是任何一种JavaScript语言支持的数据类型。

比如在上面的SampleButton中,borderWidth就是数字类型,onClick是函数类型,style的值是一个包含color字段的对象,当prop的类型不是字符串类型时,在JSX中必须用花括号{}把prop值包住,所以style的值有两层花括号,外层花括号代表是JSX的语法,内层的花括号代表这是一个对象常量。

当外部世界要传递一些数据给React组件,一个最直接的方式就是通过prop;同样,React组件要反馈数据给外部世界,也可以用prop,因为prop的类型不限于纯数据,也可以是函数,函数类型的prop等于让父组件交给了子组件一个回调函数,子组件在恰当的实际调用函数类型的prop,可以带上必要的参数,这样就可以反过来把信息传递给外部世界。

对于Counter组件,父组件ControlPanel就是外部世界,我们看ControlPanel是如何用prop传递信息给Counter的,代码如下:

class ControlPanel extends Component {
  render() {
    return (
      <div>
        <Counter caption="First" initValue={0} />
        <Counter caption="Second" initValue={10} />
        <Counter caption="Third" initValue={20} />
      </div>
    );
  }
}

ControlPanel组件包含三个Counter组件实例,在ControlPanel的render函数中将这三个子组件实例用div包起来,因为React要求render函数只能返回一个元素。

提示

虽然React声称将来可以支持render返回数组以支持多个组件,但是到本书写作时最新版v15.4为止,这个功能依然没有实现。

在每个Counter组件实例中,都使用了caption和initValue两个prop。通过名为caption的prop, ControlPanel传递给Counter组件实例说明文字。通过名为initValue的prop传递给Counter组件一个初始的计数值。

2.读取prop值

我们再来看Counter组件内部是如何接收传入的prop的,首先是构造函数,代码如下:

class Counter extends Component {
  constructor(props) {
    super(props);

    this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
    this.onClickDecrementButton = this.onClickDecrementButton.bind(this);

    this.state = {
      count: props.initValue || 0
    }
  }

如果一个组件需要定义自己的构造函数,一定要记得在构造函数的第一行通过super调用父类也就是React.Component的构造函数。如果在构造函数中没有调用super(props),那么组件实例被构造之后,类实例的所有成员函数就无法通过this.props访问到父组件传递过来的props值。很明显,给this.props赋值是React.Component构造函数的工作之一。

在Counter的构造函数中还给两个成员函数绑定了当前this的执行环境,因为ES6方法创造的React组件类并不自动给我们绑定this到当前实例对象。

在构造函数的最后,我们可以看到读取传入prop的方法,在构造函数中可以通过参数props获得传入prop值,在其他函数中则可以通过this.props访问传入prop的值,比如在Counter组件的render函数中,我们就是通过this.props获得传入的caption, render函数代码如下:

render() {
  const {caption} = this.props;
  return (
    <div>
  <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button>
  <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button>
  <span>{caption} count: {this.state.count}</span>
  </div>
  );
}

在上面的代码中,我们使用了ES6的解构赋值(destructuring assignment)语法从this. props中获得了名为caption的prop值。

3. propTypes检查

既然prop是组件的对外接口,那么就应该有某种方式让组件声明自己的接口规范。简单说,一个组件应该可以规范以下这些方面:

□ 这个组件支持哪些prop;

□ 每个prop应该是什么样的格式。

React通过propTypes来支持这些功能。

在ES6方法定义的组件类中,可以通过增加类的propTypes属性来定义prop规格,这不只是声明,而且是一种限制,在运行时和静态代码检查时,都可以根据propTypes判断外部世界是否正确地使用了组件的属性。

比如,对于Counter组件的propTypes定义代码如下:

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  initValue: PropTypes.number
};

其中要求caption必须是string类型,initValue必须是number类型。可以看到,两者除了类型不同之外,还有一个区别:caption带上了isRequried,这表示使用Counter组件必须要指定caption;而initValue因为没有isRequired,则表示如果没有也没关系。

为了验证propTypes的作用,可以尝试故意违反propTypes的规定使用Counter实例,比如在ControlPanel的render函数中增加下列的代码:

<Counter caption={123} initValue={20} />

我们在Chrome浏览器开发工具的Console界面,可以看到一个红色的警告提示,如图2-2所示。

图2-2 错误prop类型的错误提示

这段出错的含义是,caption属性预期是字符串类型,得到的却是一个数字类型。

我们尝试删掉这个Counter实例的caption属性,代码如下:

<Counter initValue={20} />

这时可以看到Console选项卡中依然有红色警告信息,如图2-3所示。

图2-3 缺失必须存在prop的错误提示

提示的含义是,caption是Counter必需的属性,但是却没有赋值。

很明显,有了propTypes的检查,可以很容易发现对prop的不正确使用方法,可尽早发现代码中的错误。

从上面可以看得出来propTypes检查可以防止不正确的prop使用方法,那么如果组件根本就没有定义propTypes会怎么样呢?

可以尝试在src/Counter.js文件中删除掉那一段给Counter.propTypes赋值的语句,在浏览器Console里可以看到红色警告不再出现。可见,没有propTypes定义,组件依然能够正常工作,而且,即使在上面propTypes检查出错的情况下,组件依旧能工作。也就是说propTypes检查只是一个辅助开发的功能,并不会改变组件的行为。

propTypes虽然能够在开发阶段发现代码中的问题,但是放在产品环境中就不大合适了。

首先,定义类的propTypes属性,无疑是要占用一些代码空间,而且propTypes检查也是要消耗CPU计算资源的。其次,在产品环境下做propTypes检查没有什么帮助,毕竟,propTypes产生的这些错误信息只有开发者才能看得懂,放在产品环境下,在最终用户的浏览器Console中输出这些错误信息没什么意义。

所以,最好的方式是,开发者在代码中定义propTypes,在开发过程中避免犯错,但是在发布产品代码时,用一种自动的方式将propTypes去掉,这样最终部署到产品环境的代码就会更优。现有的babel-react-optimize就具有这个功能,可以通过npm安装,但是应该确保只在发布产品代码时使用它。