[关闭]
@hpp157 2016-10-09T11:27:57.000000Z 字数 10388 阅读 2766

React学习手册chap6-属性,状态和组件树

Props,State,and the Component Tree

react


上一章,我们介绍了如何创建组件,首先关注的是通过组合React 组件来构建一个UI,本章介绍如何更好的使用data,缩减浪费在debug 程序上的时间。

组件树之间的数据处理是React的一大卖点,这种数据处理方式可以让你很轻松的应对bug,让你感觉程序员的生活是如此美好。如果我们可以从一个单一的位置管理数据,并建立基于该数据的用户界面,那么我们的应用会易于伸缩scale和reason about。

属性验证(property validation)

Javascript是一个弱类型的语言(a loosely typed language),意味着变量可以根据它们value改变数据类型。有效的管理变量类型将会减少我们调试代码时候的时间。

React 组件提供了一个确定和验证属性类型的方法。它可以大大减少你调试bug的时间。提供错误的属性类型会引起warnings来帮助我们查找可能出现的错误。

React为下面这些类型内置了自动属性验证:

Arrays React.PropTypes.array
Boolen React.PropTypes.bool
Functions React.propTypes.func
Numbers React.PropTypes.number
Strings Reat.PropTypes.string
Object React.PropTypes.object

在下一节,我们会为recipes创建一个Summary component,该组件会显示recipe的title,以及几个Ingredients和几个steps:
此处输入图片的描述

如上图所示,为了展示data,我们必须给Summary组件提供三个属性:a titile,an array of ingredinets,and an array of steps.
我们想验证下这些属性,确保它们是array类型并且提供一个默认的标题,当没有title的时候就会使用这个默认的title,创建组件的方式决定了验证其属性的方法(how to implement property validation depends upon how components are created),因为无状态函数式组件和ES6的classes有着不一样的验证方式。

首先,我们来看下如何使用property validation,以及如何在React.createClass()创建的组件中实现它。

通过createClass验证属性

我们需要了解为什么验证组件的属性类型是非常重要的一件事,来看下下面这个Summary component的代码:

  1. const Summary = createClass({
  2. displayName:"Summary",
  3. render(){
  4. const {ingredients,steps,title} = this.props
  5. return(
  6. <div className="summary">
  7. <h1>{title}</h1>
  8. <p>
  9. <span>{ingredients.length}Ingredients</span>
  10. <span>{steps.length}Steps</span>
  11. </p>
  12. </div>
  13. )
  14. }
  15. })

Summary component用解构赋值,从对象属性中获取ingredinets,titile,steps然后构建UI展示这些数据。因为我们希望ingredients和steps都是数组,可以用Array.length()来计算数组中item的个数。

要是我们突然用string来渲染组件会怎样呢?

  1. render(
  2. <Summary title="Peanut Butter and Jelly"
  3. Ingredients="peanut butter,jelly,bread"
  4. steps="spread peanut butter and jelly between bread"/>
  5. document.getElementById('react-container')
  6. )

Javasctipt还是会正常运行,但是查找的是string的character个数,里面的Ingredient个数和steps的步骤数完全不是我们想要的。
此处输入图片的描述

因为没有对属性进行类型的验证,所以得出了错误的Ingredient个数和steps的步骤数,如果在创建Summary 组件的时候对属性类型进行验证,React就会很容易抓获这个bug

  1. const Summary = createClass({
  2. displayName:"Summary",
  3. propTypes:{
  4. ingredients:PropTypes.array,
  5. steps:PropTypes.array,
  6. title:propTypes.string,
  7. },
  8. render(){
  9. const {ingredients,steps,title} = this.props
  10. ....

使用React内置的属性类型验证,可以保证ingredients和steps的类型都是数组array,此外,我们还可以把title的类型定为string,现在当我们传入不正确的属性类型时,控制台会发出一个error。
此处输入图片的描述

如果在渲染Summary组件的时候不给它传入任何属性会怎样呢?

  1. render(
  2. <Summary />,
  3. document.getElementById('react-container')
  4. )

会发出一个javascript 错误
此处输入图片的描述
这个错误发生是因为ingredints属性的类型是undefined,而undefined不是一个对象,没有length属性。React有一个方法来指定属性是否必需要填当这些属性没有提供的时候,react会产生一个错误:

  1. const Summary = createClass({
  2. displayName:"Summary",
  3. propTypes:{
  4. ingredients:PropTypes.array.isRequired,
  5. steps:PropTypes.array.isRequired,
  6. title:propTypes.string,
  7. }
  8. render(){
  9. const {ingredients,steps,title} = this.props
  10. .....

现在,我们不要任何属性直接对Summary组件渲染的话就会在error前出现很多warning,这样会很容易查找错误。
此处输入图片的描述

Summary组件需求的是一个ingredinets数组和一个steps数组,但是只使用了数组的lenght属性,Summary组件目的是显示ingredinets和一个steps的counts, number,因此我们用numbers类型来重构这个组件的话会更有意义,因为事实上,这个组件不需要数组类型

  1. const Summary = createClass({
  2. displayName:"Summary",
  3. propTypes:{
  4. ingredients:PropTypes.number.isRequired,
  5. steps:PropTypes.number.isRequired,
  6. title:propTypes.string,
  7. },
  8. render(){
  9. const {ingredients,steps,title} = this.props
  10. return(
  11. <div className="summary">
  12. <h1>{title}</h1>
  13. <p>
  14. <span>{ingredients.length}Ingredients</span>
  15. <span>{steps.length}Steps</span>
  16. </p>
  17. </div>
  18. )
  19. }
  20. })

为这个组件使用number类型是一个更灵活的方法。现在Summary组件只显示UI,至于计算ingredientssteps个数的活就交给父组件吧。

默认属性

另一个改善Summary组件的方法是给它设置默认属性,当你没有为组件提供属性的时候,组件就会使用它的默认的属性。

比如,我们想要Summary在没有提供属性的时候也能正常运行:

  1. import {render} from 'react-dom'
  2. render(
  3. <Summary />,
  4. document.getElementById('react-container')
  5. )

通过 createClass,我们可以添加一个叫做getDefaultProps的方法,如果你不给组件传递值的话,它会取默认值。

  1. const Summary = createClass({
  2. displayName:"Summary",
  3. propTypes:{
  4. ingredients:PropTypes.number,
  5. steps:PropTypes.number,
  6. title:propTypes.string,
  7. },
  8. getDefaultProps(){
  9. return {
  10. ingredients:0,
  11. steps:0,
  12. title:"[recipe]"
  13. }
  14. },
  15. render(){
  16. const {ingredients,steps,title} = this.props
  17. return(
  18. <div className="summary">
  19. <h1>{title}</h1>
  20. <p>
  21. <span>{ingredients}Ingredients |</span>
  22. <span>{steps}Steps</span>
  23. </p>
  24. </div>
  25. )
  26. }
  27. })

现在,我们再次渲染这个组件(不传递属性),结果如下图所示:
此处输入图片的描述

自定义属性验证

React中内置的验证有时候不能满足我们的需求,比如,你想验证一个number是否在一个指定的范围内,或者一个value中是否包含字符串。因此,React也允许我们自己定义规则来验证属性类型。

自定义属性验证是用一个函数来实现的,验证的时候函数要么返回一个error,要么就是null,

用基本的属性类型验证,你只能一个条件验证一个属性。而自定义属性就可以让我们用许多不同的方法来验证属性,在自定义属性类型验证这个函数中,我们首先检查属性是否是string然后把属性限制在20个字符以内。

  1. ingredients:PropTypes.number,
  2. steps:PropTypes.number,
  3. title:(props,propName)=>
  4. (typeof props[propName] !=='string')?
  5. new Error("A title must be a string"):
  6. (props[propName].length>20)?
  7. new Error('title is over 20 characters'):
  8. null
  9. }

所有的属性验证都是函数,要完成一个自定义的验证器,我们要在propType对象下给回调函数设置一个title属性值。

当渲染组件的时候,react会把props对象和当前属性的名称name作为参数传到函数里,我们可以用哪些参数来检查一个属性的值。

自定义属性验证器允许你实现具体的自定的一些验证规则,当验证结果不符合规则时,它仅仅返回(only return )一个error,当你使用或者复用组件的时候,使用自定义的验证器也是一个防止错误的好方法。

ES6的类和无状态函数式组件(ES6 classes and stateless Functional Components)

在前面的一小节中,我们发现,propTypes和defaultProps可以被加入到我们的使用React.createClass组件类中。这种类型的检查也可用于ES6类和无状态函数式组件中,但语法稍有不同。

当使用ES6类的时候,propTypes和defaultProps的声明是定义在类实例上的(on the class instance),也就是类主体的外部。只要一个类被定义了,我们就可以直接设置propType对象和defaultProps对象了。

  1. class Summary extends React.Component{
  2. render(){
  3. const {ingredients,steps,title} = this.props
  4. return(
  5. <div className="summary">
  6. <h1>{title}</h1>
  7. <p>
  8. <span>{ingredients}Ingredients |</span>
  9. <span>{steps}Steps</span>
  10. </p>
  11. </div>
  12. )
  13. }
  14. }
  15. Summary.propType={
  16. ingredients:PropTypes.number,
  17. steps:PropTypes.number,
  18. title:(props,propName)=>
  19. (typeof props[propName] !=='string')?
  20. new Error("A title must be a string"):
  21. (props[propName].length>20)?
  22. new Error('title is over 20 characters'):
  23. null
  24. }
  25. Summary.defaultProps={
  26. ingredients:0,
  27. steps:0,
  28. title:"[recipe]"
  29. }

propType和defaultProps这两个object literal也可以添加到无状态函数式组件中,我们可以直接在函数参数中设置默认值

  1. const Summary =({ ingredients=0,steps=0,title="[recipe]" }) =>{
  2. return <div>
  3. <h1>{title}</h1>
  4. <p>{ingredients}Ingredients |{steps}steps</p>
  5. </div>
  6. }
  7. Summary.propType={
  8. ingredients:PropTypes.number.isRequired,
  9. steps:PropTypes.number.isRequired,
  10. }
  11. Summary.defaultProps={
  12. ingredients:1,
  13. steps:1
  14. }

class的静态属性

在前一节,我们了解了怎么在类class的外部定义defaultProps和propTypes。还有另一种是新兴的,ECMAScript的规范的最新建议之一:类字段和静态属性。
class的静态属性可以让我们在clsss内部封装propTypes,defaultProps

  1. class Summary extends React.Component{
  2. static propTypes={
  3. ingredients:PropTypes.number,
  4. steps:PropTypes.number,
  5. title:(props,propName)=>
  6. (typeof props[propName] !=='string')?
  7. new Error("A title must be a string"):
  8. (props[propName].length>20)?
  9. new Error('title is over 20 characters'):
  10. null
  11. }
  12. static defaultProps={
  13. ingredients:0,
  14. steps:0,
  15. title:"[recipe]"
  16. }
  17. render(){
  18. .....
  19. }
  20. }

Refs

References或者叫做refs,是一个允许组件和children元素交流互动的特性。refs最常见的使用场景就是和一个UI交互,这个UI能够收集用户传入的数据(比如表单)。

本章剩下的章节,我们做一个可以让用户保存和管理color的十六进制的值的应用。这个应用叫做color organizer,可以让user向一个list中添加颜色,list生成之后,可以被删除。

首先我们需要一个Color Form表单来收集用户输入的color信息,user可以提供color的title,hex value,

  1. import { Component } from 'react'
  2. class AddColorForm extends Component {
  3. render() {
  4. return (
  5. <form onSubmit={e=>e.preventDefault()}>
  6. <input type="text"
  7. placeholder="color title..." required/>
  8. <input type="color" required/>
  9. <button>ADD</button>
  10. </form>
  11. )
  12. }
  13. }

这个AddColorForm组件渲染一个HTML 表单,它包含三个元素,text输入框用来输入颜色的名称,颜色输入框用来输入颜色的值,还有一个提交按钮。点击提交的时候会有一个专门处理的函数来接管这些信息。这个处理函数能够组织表单发送HTTP请求。

一旦表单被渲染了,我们就需要和表单进行互动。详细点说,就是当表单第一次提交时,我们需要收集新的color信息和对表单重置以便用户们输入新的颜色。使用refs,我们可以refer to the title和color元素,并且和它们进行互动。

  1. import {Component} from 'react'
  2. class AddColorForm extends Component{
  3. constructor(props){
  4. super(props)
  5. this.submit = this.submit.bind(this)
  6. }
  7. submit(e){
  8. const {_title,_color} = this.refs
  9. e.preventDefault();
  10. alert('New Color:${_title.vlaue} ${_color.value}')
  11. _title.value='';
  12. _color.value='#000000';
  13. _title.focue();
  14. }
  15. render(){
  16. return (
  17. <from onSubmit={this.submit}>
  18. <input ref="_title"
  19. type="text"
  20. placeholder="color title... " required/>
  21. <input ref="_color"
  22. type="color" required/>
  23. <button>ADD</button>
  24. )
  25. }
  26. }

我们需要为这个ES6组件添加一个构造器(contructor),因为我们把submit移到了自己的函数里(moved submit to its own function),用ES6组件的classes,哪个方法需要访问组件,我们就要用this把组件的作用域绑定过去。(with ES6 component classes,we must bind the scope of the component to any methods that need to access that scope with this)

接着,在render方法里,我们把表单的onSubmit事件指向组件的submit方法,我们也给想要引用(reference)的组件添加了一个ref字段,React使用ref来reference DOM元素,给input的ref属性添加_title,_color意思是我们可以用this.refs._title或者this.refs._color来访问这些元素。(Adding _title and _color to the input’s ref attribute means that we can access those elements with this.refs._title or this.refs_color.)

当用户添加里一个新的title,选择了一个新的color,以及提交里表单后,组件的submit方法就会被调用(be invoked to) 来处理这个事件。在我们阻止了表单默认的submit行为之后,我们向用户发出一个弹窗,在里面显示通过refs收集到的数据。当用户关闭弹窗后,refs再度被使用,用来重置表单,给title字段获得焦点。

如果使用React.createClass来创建组件的话,就不需要把this作用域绑定到你的组件方法上里,它会自动的为你进行绑定。

双向数据绑定(Two-way Data Binding)

只是让一个表单能够在弹窗中显示input中的数据还不够酷,我们需要的是它除了从用户那里收集数据,还应该能自动的把数据发到服务端。发到服务端这块内容我们将在12章进行讲解,现在,我们要能从AddForm这个组件中收集数据并把它发送出去。

从React组件中收集数据常用的解决方法是用双向数据绑定。它涉及向组件component发送一个回调函数作为属性,组件呢是个老好人,投之以桃,报之以礼,别人给它一个回调函数作为参数,它就把data作为函数的参数传回去。双向数据绑定就是我们给组件发送一个函数作为组件属性,组件发送数据回来作为函数的参数。

比如说,我们想用color form,当用户提交一个新的颜色时我们想收集这个信息并且在控制台上打印出来信息。

  1. const logColor = (title, color) =>
  2. console.log(`New Color: ${title} | ${value}`)
  3. <AddColorForm onNewColor={logColor} />

使用双向数据绑定的话,我们可以创建一个logColor函数,该函数有title和color两个参数。我们只需要给onNewColor添加一个函数属性并且把它们发送给logColor函数就行了。这样,当用户添加一个新的color,logColor函数就被调用,并且我们已经发送了一个函数作为属性。(When the user adds a new color, logColor is invoked, and we’ve sent a function as a property.)

为了实现数据双向绑定,我们需要用适当的数据从props中调用onNewColor

  1. submit(){
  2. const {_title,_color}=this.refs
  3. this.props.onNewColor(_title.value,_color.value)
  4. _title.value='',
  5. _color.value='#000000',
  6. _title.focus()
  7. }

在组件中,我们用this.props.onNewColor()函数来代替弹窗,并且传递我们已经从refs中得到的新的title和新的color的value。

AddColorForm组件的角色就是收集数据并传递下去。我们现在能用这个form收集数据,并把它传递给其它的组件或方法来处理这个数据。

  1. <AddColorForm onNewColor={(title,color)=> {
  2. console.log('TODO:add new ${title} and ${color} to the list')
  3. console.log('dodo:render UI with new color')
  4. }}/>

无状态函数式组件中的Refs

refs也可以用在无状态函数式组件中,这些组件并没有this,所以不能用this.refs,用string属性来代替,该函数将input实例作为参数。我们可以捕捉实例,并将其保存到一个局部变量中。

  1. let _title, _color
  2. const submit=e=>{
  3. e.preventDefault()
  4. onNewColor(_title.value, _color.value)
  5. _title.value='',
  6. _color.value='#000000',
  7. _title.focus()
  8. }
  9. return(
  10. <form onSubmit={submit}>
  11. <input ref={input=>_title=input}
  12. type="text"
  13. placeholder="color title ..." required />
  14. <input ref={input=>_color=input}
  15. type="color" required />
  16. </form>
  17. )
  18. }

在这个组件中,refs用回调函数设置的而不是string,回调函数传递元素的instance作为参数,该instance可以被捕获,并且存到一个局部变量中(_titile,_color),一旦我们保存refs到局部变量中,当表单提交的时候就很容易访问。

React状态管理

目前为止,我们仅用属性来处理react组件中的数据,属性是不可变的,我们渲染的时候,组件的属性并不会变化,为了让我们UI变化,我们需要其它的机制来用新的属性预渲染组件树,React的state是内置的选项,用来管理组件中将要变化的数据。当应用的state变化了,UI会立马反应这种变化。

程序的UI是可以增删改查的,在React中,UI反应应用的state变化。

状态在React中可以用一个单独的Javascript对象来表示。当一个组件的state改变了,组件就立即渲染一个新的UI来反应这种变化。

来让我们看下组件中的state是怎么使用的吧。

组件状态介绍。

State代表了组件中我们想要改变的数据-data。为了给大家演示,我们来看下StarRating组件。

这个组件需要两个数据:一个star的总计个数,还有就是要高亮显示的星星的个数。

我们需要一个可点击的组件,它有一个已经选中的属性,每个Star都有一个无状态函数式组件。

  1. const Star=({selected=false,onClick=f=>f }) =>
  2. <div className={(selected)?"star selected":"star"}
  3. onClick={onClick}>
  4. </div>
  5. Star.propTypes={
  6. selected:PropTypes.bool,
  7. onClick:PropTypes.func
  8. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注