React组件详解

2022-11-30 11:39:40 浏览数 (1)

3.6.1 React组件简介

众所周知,组件作为React的核心内容,是View的重要组成部分,每一个View页面都由一个或多个组件构成,可以说组件是React应用程序的基石。在React的组件构成中,按照状态来分可以分为有状态组件和无状态组件。 所谓无状态组件,就是没有状态控制的组件,只做纯静态展示的作用,无状态组件是最基本的组件形式,它由属性props和渲染函数render构成。由于不涉及到状态的更新,所以这种组件的复用性也最强。 有状态组件是在无状态组件的基础上增加了组件内部状态管理,有状态组件通常会带有生命周期lifecycle,用以在不同的时刻触发状态的更新,有状态组件被大量用在业务逻辑开发中。

目前,React支持三种方式来定义一个组件,分别是: - ES5的React.createClass方式; - ES6的React.Component方式; - 无状态的函数组件方式。

在ES6出现之前,React使用React.createClass方式来创建一个组件类,它接受一个对象作为参数,对象中必须声明一个render方法,render函数返回一个组件实例。例如:

代码语言:javascript复制
import React from 'react';

const TextView = React.createClass({  
//初始化组件状态
getInitialState () {
    return {
    };
  },

  render() {
    return (
      <div>我是一个Text</div>
    );
  }
});

export default TextView; 

不过,随着React版本的持续升级,ES5的React.createClass方式暴露的问题也越来越多。例如,使用React.createClass创建的组件,事件函数会自动绑定相关的函数,这样会导致不必要的性能开销,而React.Component则是有选择性的绑定有需要函数。 随着ES6语法的普及,React.createClass正逐渐被React.Component方式所替代。并且,使用React.Component方式创建的组件更符合面向函数编程的思想,可读性也更高。例如,下面是使用React.Component方式创建TextView的实例。

代码语言:javascript复制
import React,{Component} from 'react'

class TextView extends Component {
//初始化组件状态
constructor(props) {
    super(props);    //传递props给component
    this.state = {
    };
  }

    render() {
        return (
            <div>我是一个Text</div>
        );
    }
}

export default TextView;

通过React.createClass和React.Component方式创建的组件都是有状态的组件,而无状态组件则是通过无状态的函数创建的。无状态组件是React在0.14版本推出的一种新的组件形式,它是一种只负责展示的纯组件。 在React开发中,随着应用复杂度的不断提升和组件数量的增加,组件的管理和维护成为不得不面对的问题,于是一种只负责展示的纯组件出现了。无状态组件的特点是不需要管理组件状态state,数据直接通过props传入即可,这也符合React单向数据流的思想。 对于无状态组件的函数式声明方式,不仅可以提高代码的可阅读性,还能大大的减少代码量,提高代码的复用率,箭头函数则是函数式编程的最佳搭档。

代码语言:javascript复制
const Todo = (props) => (
  <li
    onClick={props.onClick}
    style={{textDecoration: props.complete ? "line-through" : "none"}}>
    {props.text}
  </li>
)

对于上面定义的Todo组件,输入输出数据完全由props决定,如果props为Object类型时,还可以使用ES6提供的解构赋值。例如:

代码语言:javascript复制
const Todo = ({ onClick, complete, text, ...props }) => (
  <li
    onClick={onClick}
    style={{textDecoration: complete ? "line-through" : "none"}}
    {...props}
  >
    {props.text}
  </li>
)

无状态组件一般会搭配高阶组件(简称OHC)一起使用,高阶组件主要用来托管State,Redux框架就是通过store来管理数据源和组件的所有状态,其中所有负责展示的组件都使用无状态函数式的写法,无状态组件也被大规模的使用在大型应用程序中。 虽然,无状态组件具有诸多的优势,但也不是万能的。比如,无状态组件在被React调用之前,组件是不会被实例化的,所以它不支持ref特性。

3.6.2 ES5与ES6组件对比

相比React.createClass方式,React.Component带来了诸多语法上的改进: 1. import ES6使用import方式替代ES5的require方式来导入模块,其中import { }可以直接从模块中导入变量名,此种写法更加简洁直观。 2. 初始化state 在ES6的语法规则中,React的组件使用的类继承的方式来实现,去掉了ES5的getInitialState的hook函数,state的初始化则放在constructor构造函数中声明。 3. this绑定 使用React.createClass方式创建的组件,事件函数会自动绑定this函数,但是此种方式会带来不必要的性能开销,增加了代码过时的可能性。而使用React.Component方式创建组件时,事件函数并不会自动绑定this函数,需要开发者手动绑定,从而减少了不必要的性能开销。 4. 默认属性和状态 使用React.createClass方式创建组件时,有关组件props的属性类型及组件默认属性会作为组件实例的属性进行配置,其中defaultProps可以使用组件的getDefaultProps方法来获取。例如:

代码语言:javascript复制
const Demo = React.createClass({
    propTypes: { 
        name: React.PropTypes.string
    },
    getDefaultProps(){   
        return {
            name: ' '    
        }
    }
    …
})

而使用React.Component方式创建组件时,配置组件的属性时,是通过类的静态属性来配置的。例如:

代码语言:javascript复制
class Demo extends React.Component {
//类的静态属性
    static propTypes = {   
        name: React.PropTypes.string
    };
    //类的静态属性
    static defaultProps = {
        name: ' '
    };
    ...
}

同时,React.createClass方式创建的组件,组件的状态通过getInitialState方法来进行配置。而React.Component方式创建的组件,其状态state则是在constructor函数中像初始化组件属性一样进行声明的。

3.6.3 组件的props

React组件化的开发思路一直为人所称道,而组件最核心的两个概念莫过于props与state,组件的最终呈现效果正是props和state作用的结果。其中,props是组件对外的接口,而state则是组件对内的接口。一般情况下,props是不变的,其基本的使用方法如下。

代码语言:javascript复制
{this.props.key}

在典型的React数据流模型中,props是父子组件交互的唯一方式,下面的例子演示了如何在组件中使用props。

代码语言:javascript复制
class HelloMessage extends Component{
    constructor(props){
        super(props);
        this.state = {
            name: 'jack'
        }
    }

    render(){
        return (
            <h1> Hello {this.props.name}</h1>
        )
    }
}
export default Message;

在上面的例子中,通过构造函数为属性设置初始值,当然也可以不设置初始值,当需要使用name属性的时候可以通过{this.props.name}方式获取。在ES5语法中,如果想要为组件的属性设置默认值,需要通过getDefaultProps()方法来设置。例如:

代码语言:javascript复制
var HelloMessage = React.createClass({
  //设置初始值
  getDefaultProps: function() {
    return {
      name: 'jack'
    };
  },
  render: function() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

ReactDOM.render(
  <HelloMessage />,
  document.getElementById('example')
);

props作为父子组件沟通的桥梁,为组件的通信和传值提供了重要手段,下面是一个父子组件传值的实例。

代码语言:javascript复制
//子组件
export default class Child extends Component {

    constructor(props){
        super(props);
        this.state={
            counter:props.age||0
        }
    }

    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    }
}

Child.propTypes={
    name:PropTypes.string.isRequired,
    age:PropTypes.number
}

Child.defaultProps={
    age:0
}

当父组件需要向子组件传递值时,只需要引入子组件,然后使用组件提供的props属性即可。例如:

代码语言:javascript复制
//父组件
export default class Father extends Component {

    render() {
        return (
            <div>
                <Child name="jack" age={30}/>
                <Child name="tom" age={20}/>
            </div>
        )
    }
}

在上面的实例中,子组件props接受的数据格式由PropTypes进行检测,并且使用isRequired关键字来标识该属性是否是必须的。props使用PropTypes来保证传递数据的类型和格式,当向props传入无效数据时,JavaScript的控制台会给出警告提示。

3.6.4 组件的state

如果说props是组件对外的接口,那么state则是组件对内的接口,state作为组件的私有属性,只能被本组件去访问和修改。而props对于使用它的组件来说是只读的,如果想要修改props,只能通过组件的父组件修改。 React把组件看成是一个特殊的状态机,通过与用户的交互实现不同状态,进而渲染界面,让用户界面和数据保持一致。在React中,如果需要使用state,就需要在组件的constructor初始化相关的state。例如:

代码语言:javascript复制
constructor(props) {
   super(props);
   this.state={
      key:value,
      ...
   }
}

如果要更新组件的state,则需要调用setState方法。

代码语言:javascript复制
this.setState({
     key:value
 }) ;

需要注意的是,在调用setState函数执行更新操作时,组件的state并不会立即改变,因为setState()是异步的。setState操作只是把要修改的状态放入一个队列中,出于性能原因,React可能会对多次的setState状态修改进行合并修正,所以当我们使用{this.state}获取状态state时,可能并不是我们需要的那个state。同理,也不能依赖当前的props来计算组件的下一个状态,因为props一般也是从父组件的State中获取,依然无法确定组件在状态更新时的值。 同时,在调用setState修改组件状态时,只需要传入需要改变的状态变量即可,而不必传入组件完整的state,因为组件state的更新是一个浅合并的过程。例如,一个组件的state由title和content构成。

代码语言:javascript复制
this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

当需要修改title的状态时,只需要调用setState()修改title的内容即可。例如:

代码语言:javascript复制
this.setState({title: 'React Native'});

由于state的更新是一个浅合并的过程,所以合并后的state只会修改新的title到state中,同时保留content的原有状态。合并后的内容如下:

代码语言:javascript复制
{
  title : 'React Native ',
  content : 'React is an wonderful JS library!'
}

3.6.5 组件的ref

在React典型的数据流模型中,props作为父子组件交互的最基本也是最重要的方式,主要通过传递props值来使子组件重新render,从而达到父子组件通信的目的。当然,在某些特殊的情况下修改子组件的时候就需要是要另一种方式(例如和第三方库的DOM整合或者某个DOM元素focus问题上),即是ref方式。 React提供的ref属性,其本质就是调用ReactDOM.render()返回的组件实例,用来表示为对组件真正实例的引用。具体使用时,可以将它绑定到组件的render()上,然后就可以用它输出组件的实例。 ref不仅可以挂载到组件上,还可以作用于DOM元素上。具体来说,挂载组件使用class定义,表示对组件实例的引用,此时不能在函数式组件上使用ref属性,因为它们不能获取组件的实例。而挂载到DOM元素时则表示具体的DOM元素节点。 ref支持两种调用方式:一种是设置回调函数,另一种是字符串的方式。其中,设置回调函数是官方的推荐方式,使用它可以更细致的控制refs,使用此种方式,ref属性接受一个回调函数,它在组件被加载或者卸载时被立即执行。具体来说,当给HTML元素添加ref属性时,Refs回调接受底层的Dom元素作为参数,当组件卸载时Refs回调会接受null作为参数。

代码语言:javascript复制
class Demo extends React.Component{
  constructor(props) {
    super(props);
    this.state = {           
      isInputshow:false    //控制input是否渲染
    }
  }

  inputRefcb(instance){
    if(instance) {                            
      instance.focus();                     
    }
  }

  render() {
   {
      this.state.isInputshow ? 
      <div>
        <input ref={this.inputRefcb} type="text" />
      </div>
      : null           
    }
  }
}

对于上面的例子,触发回调的时机主要有以下三种情况:

  • 组件被渲染后,回调参数instance作为input的组件实例的引用,回调参数可以立即使用该组件;
  • 组件被卸载后,回调参数instance此时为null,这样做可以确保内存不被泄露;
  • ref属性本身发生改变,原有的ref会再次被调用,此时回调参数instance变成具体的组件实例。

如果在使用String方式,则可以通过{this.refs.inputRef}的方式来获取组件实例。例如:

代码语言:javascript复制
class Demo extends React.Component{
  constructor(props) {
    super(props);

    onFocus(){
     this.refs.inputRef.focus()
    }
  }
  render() {
    <div>
      <input ref="inputRef" type="text" />
    </div>
    <input type="button" value="Focus" onClick={this.onFocus} />
  }
}

同时,官方明确申明不能在函数式声明组件中使用ref,因为它们不能获取组件的实例。例如,下面的实例是错误的:

代码语言:javascript复制
function InputComponent() {
  …
  return <input />;
}

class Demo extends React.Component {
  render() {
    // 编译不通过
    return (
      < InputComponent
        ref={(input) => { this.textInput = input; }} />
    );
  }
}

在某些情况下,可能需要从父组件中访问子组件的DOM节点,那么可以在子组件中暴露一个特殊的属性给父组件调用,子组件接收一个函数作为prop属性,同时将这个函数赋予到DOM节点作为ref属性,那么父组件就可以将它的ref回调传递给子级组件的DOM。这种方式对于class声明的组件和函数式声明的组件都是适用的。例如:

代码语言:javascript复制
function TextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Father extends React.Component {
  render() {
    return (
//子组件传入inputRef函数
      < TextInput  inputRef={e => this.inputElement = e}  />  
    );
  }
}

在上面的例子中,父组件Father将他的ref回调函数通过inputRef属性传递给TextInput,而TextInput将这个回调函数作为input元素的ref属性,此时父组件Father中通过{this.inputElement}得到子组件的input对应的DOM元素。暴露DOM的ref属性除了可以方便在父组件中访问子组件的DOM节点外,还可以实现多个组件跨层级调用。

0 人点赞