(React启蒙)理解React 组件

本文将首先讲述如何经过React nodes建立基础的React组件,而后进一步剖析React组件内部的点滴,包括该如何理解React组件,获取React组件实例的两种办法,React事件系统,对React生命周期函数的理解,获取React组件的子组件和子节点的方法,字符串ref和函数式ref,以及触发React组件从新渲染的四种方法。
本文是React启蒙系列的第六章,依旧讲的是React的基础使用方法,可是若是你对上面提到的概念有不理解或不熟悉的地方,跳到对应地方观看阅读,你应该会能有所收获。html

理解React组件

在具体说明如何建立React组件的语法以前,对什么是React组件,其存在的意思及其划分依据等作一个论述是颇有必要的。node

咱们设想如今有一个webApp,这个app能够用来实现不少功能,依据功能,咱们能够把其划分为多个功能碎片。要实现这么一个功能碎片,可能须要更多更小的逻辑单元,甚至还能够继续分。而咱们编程其实就是在有一个整体轮廓的前提下,经过解决一个个小小的问题来解决一个小问题,解决一个个小问题来实现软件的开发。React组件就是这样,你能够就把它当作一个个可组合的功能单元。react

以一个登录框为例,登陆框自己就是网站的一个组件,可是其内包含诸如文本输入框,登录按钮等,固然若是你想要作的只是最基础的功能,输入框和按钮等能够只是一个个React 节点,可是若是你想为输入框加上输入检测,输入框可能就有必要写成一个单独的组件了,这样也有利于复用,以后须要作的可能只是简单的经过props传入不一样的参数就能够实现不一样的检测。假想咱们如今的登陆框组件,包含React <Button>元素造成登陆按钮,也包含多个文本输入检测组件。那么父组件的做用一方面在于聚合小组件造成更复杂的功能单元,另外一方面在于为子组件信息的沟通提供渠道(好比说在知足必定的输入条件后,登陆按钮的状态从不可点击变为可点击)。git

建立React组件

React组件经过调用React.createClass()方法建立,该方法须要传入一个对象形式的参数。在该对象中能够为所建立组件配置各类参数,其可用参数以下表:github

方法(配置参数)名称 描述
render() 必填,一般为一个返回React nodes或者其它组件的函数
getInitialState() 一个用于设置最初的state的函数,返回一个对象
getDefaultProps() 一个用于设置默认props的函数,返回值为一个对象
propTypes 一个用于验证特定props类型的对象
mixins 组件间共享方法的途径
statics 一个由多个静态方法组成的对象,静态方法中不能直接调用propsstate(可经过参数)
displayName 是一个用于命名组件的字符串,用于展现调试信息,使用JSX时将自动设置??
componentWillMount() 在组件首次渲染前触发,只会触发一次
componentDidMount() 在组件首次渲染后触发,只会触发一次
componentWillReceiveProps() 在组件将接受新props时触发
shouldComponentUpdate() 组件再次渲染前触发,可用于判断是否须要再次渲染
componentWillUpdate() 组件再次渲染前当即触发
componentDidUpdate() 组件渲染后当即触发
componentWillUnmount() 组件卸载前当即触发

在上述因此方法中,最重要且必不可少的是render(),它的做用是返回React节点和组件,其它全部的方法是可选的。web

实际写一个例子总比空说要容易理解,如下是使用React的React.createClass()建立的Timer组件编程

var Timer = React.createClass({ 
    getInitialState: function() { 
        return {
            secondsElapsed: Number(this.props.startTime) || 0
        };
    },
    tick: function() { //自定义方法
        this.setState({
            secondsElapsed: this.state.secondsElapsed + 1
        });
    },
    componentDidMount: function() {//生命周期函数
        this.interval = setInterval(this.tick, 1000);
    },
    componentWillUnmount: function() {//生命周期函数
        clearInterval(this.interval);
    },
    render: function() { //使用JSX返回节点
        return (
            <div>
                Seconds Elapsed: {this.state.secondsElapsed}
            </div>
        );
    }
});

ReactDOM.render(< Timer startTime = "60" / >, app); //pass startTime prop, used for state

点击JSFiddle查看效果数组

如今若是对上述组件建立的代码有所疑惑也没关系,本文接下来将一步步的介绍上述代码中设计都的各个概念,包括this,生命周期函数,React返回值的格式,如何在React中自定义函数,以及React组件中事件的定义等等。浏览器

在此须要注意的是组件名是以大写开头的。babel

当一个组件被建立(挂载)之后,咱们就可使用组件的API了,一个组件包含如下四个API
this.setState()

this.setState({mykey: 'my new value'});  
this.setState(function(previousState, currentProps) 
        { return {myInteger: previousState.myInteger + 1};
         });

做用:

用以从新渲染组件或者子组件

replaceState()

this.replceState({mykey: 'my new value'});

做用:

效果和`setState()`相似,不过并不会和老的状态合并,而是直接删除老的状态,应用新的状态。

forceUpdate()

this.forceUpdate(function(){//callback});

做用:

调用此方法将跳过组件的`shouldComponentUpdate()`事件,直接调用`render()`

isMounted()

this.isMounted()

做用

判断组件是否被挂载在DOM中,组件被挂载返回`true`,不然返回`false`

最经常使用的组件API是setState(),后文还会细讲。

小结

  • componentWillUnmount, componentDidUpdate, componentWillUpdate, shouldComponentUpdate, componentWillReceiveProps, componentDidMount, componentWillMount等方法被称做React 组件的生命周期函数,它们会在组件生命过程的不一样阶段被触发。

  • React.createClass()是一个方便的建立组件实例的方法;

  • render()方法应该保持纯洁;

render()方法中不能更改组件状态

React组件的返回值

上文已经提到每一个React组件必须有的方法就是render(),这个方法的返回值只能是一个react 节点或一个react组件,这个节点或组件中能够包含任意多的子节点或者子元素。在下面的例子中咱们能够看到在<reactNode>中包含了多个子节点。

var MyComponent = React.createClass({
  render: function() {
    return <reactNode> <span>test</span> <span>test</span> </reactNode>;
  }
});

ReactDOM.render(<MyComponent />, app);

值得注意的地方在于,若是你想返回的react 节点超过一行,应该用括号把返回值包围起来,以下所示

var MyComponent = React.createClass({
  render: function() {
    return (
        <reactNode> 
            <span>test</span>
            <span>test</span> 
        </reactNode>
    );
  }
});

ReactDOM.render(<MyComponent />, app);

另外一个值得注意的地方是返回值最外层不能出现多个节点(组件),否者会报错

var MyComponent = React.createClass({
  render: function() {
    return (
            <span>test</span>
            <span>test</span> 
    );
  }
});

ReactDOM.render(<MyComponent />, app);

上述代码就会报错,报错信息以下

babel.js:62789 Uncaught SyntaxError: embedded: Adjacent JSX elements must be wrapped in an enclosing tag (10:3)
   8 |     return (
   9 |             <span>test</span>
> 10 |             <span>test</span>
     |    ^
  11 |     );
  12 |   }
  13 | });

通常来讲开发者会在最外层加上一个<div>元素包裹其它节点以免此类错误。

一样,若是return()中的最外层出现了多个组件,也会出错。

获取组件实例的两种方法

当一个组件被render后,一个组件便经过传入的参数实例化了,咱们有两种办法获取这个实例及其内部属性(this.propsthis.setState())。

第一种方法就是使用this关键字,在组件内部的方法中使用this咱们发现,这个this指向的就是该组件实例。

var Foo = React.createClass({
    componentWillMount:function(){ console.log(this) },
    componentDidMount:function(){ console.log(this) },
    render: function() {
        return <div>{console.log(this)}</div>;
    }
});

ReactDOM.render(<Foo />, document.getElementById('app'));

获取某组件实例的另一种方法是调用ReactDOM.render()方法,这个方法的返回值是最外层的组件实例。
看以下代码能够更好的理解这句话

var Bar = React.createClass({
    render: function() {
        return <div></div>;
    }
});

var foo; //store a reference to the instance outside of function

var Foo = React.createClass({
    render: function() {
        return <Bar>{foo = this}</Bar>;
    }
});

var FooInstance = ReactDOM.render(<Foo />, document.getElementById('app'));

console.log(FooInstance === foo); //true,说明返回值和指向一致

小结
this的最多见用法就是在一个组件内调用该组件的各个属性和方法,如this.props.[NAME OF PROP]this.props.children,this.state,this.setState(),this.replaceState()等。

在组件上定义事件

第四章和第五章已经屡次介绍过React的事件系统,事件能够被直接添加都React节点上,下面的代码示例中,咱们添加了两个React事件(onClick&onMouseOver)到React<div>节点中

var MyComponent = React.createClass({
    mouseOverHandler:function mouseOverHandler(e) {
            console.log('you moused over');
            console.log(e); //e is sysnthetic event instance
        },
    clickHandler:function clickhandler(e) {
            console.log('you clicked');
            console.log(e); //e is sysnthetic event instance
        },
    render:function(){
        return (
<div onClick={this.clickHandler} onMouseOver={this.mouseOverHandler}>click or mouse over</div>
        )
    }
});

ReactDOM.render(<MyComponent />, document.getElementById('app'));

点击JSFiddle查看效果

事件能够被看作是特殊的props,只是React对这些特殊的props的处理方式和普通的props有所不一样。
这种不一样表如今会自动为事件的回调函数绑定上下文,在下面的示例中,回调函数中的this指向了组件实例自己。

var MyComponent = React.createClass({
    mouseOverHandler:function mouseOverHandler(e) {
            console.log(this); //this is component instance
            console.log(e); //e is sysnthetic event instance
        },
    render:function(){
        return (
            <div onMouseOver={this.mouseOverHandler}>mouse over me</div>
        )
    }
});

ReactDOM.render(<MyComponent />, document.getElementById('app'));

React所支持的因此事件可见此表

小结

  • React规范化了事件在不一样浏览器中的表现,你能够放心的跨浏览器使用;

  • React事件默认在事件冒泡阶段(bubbling)触发,若是想在事件捕获阶段触发须要在事件名后加上Capture(如onClick变为onClickCapture);

  • 若是你想获知浏览器事件的详情,你能够经过在回调函数中查看SyntheticEvent对象中的nativeEvent值;

  • React实际上并未直接为React nodes添加事件,它使用的是event delegation事件委托机制

  • 想要阻止事件冒泡,须要手动调用e.stopPropagation()e.preventDefault(),不要直接使用returning false,

  • React其实并无支持全部的JS事件,不过它还提供额外的生命周期函数以供使用React lifecycle methods.

组件组合

React组件的render()方法中能够包含对其它组件的引用,这使得组件之间能够嵌套,通常咱们把被嵌套的组件称为嵌套组件的子组件。

下例中组件BadgeList包含了BadgeBill和BadgeTom两个组件。

var BadgeBill = React.createClass({
    render: function() {return <div>Bill</div>;}
});

var BadgeTom = React.createClass({
    render: function() {return <div>Tom</div>;}
});

var BadgeList = React.createClass({
    render: function() {
        return (<div>
            <BadgeBill/>
            <BadgeTom />
        </div>);
    }
});

ReactDOM.render(<BadgeList />, document.getElementById('app'));

此处为展现嵌套关系,代码有所简化。

小结

  • 编写可维护性UI的关键之一在于可组合组件,React组件自然适用这一原理;

  • render方法中,组件和HTML能够组合使用;

React组件的生命周期函数

每一个组件都具备一系列的发生在其生命中不一样阶段的事件,这些事件被称为生命周期函数。

生命周期函数能够理解为React为组件的不一样阶段提供了的钩子函数,用以更好的操做组件,下例是一个定时器组件,其在不一样生命周期函数中执行了不一样的事件

var Timer = React.createClass({
    getInitialState: function() { 
        console.log('getInitialState lifecycle method ran!');
        return {secondsElapsed: Number(this.props.startTime) || 0};
    },
    tick: function() {
        console.log(ReactDOM.findDOMNode(this));
        if(this.state.secondsElapsed === 65){
            ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);
            return;
        }
        this.setState({secondsElapsed: this.state.secondsElapsed + 1});
    },
    componentDidMount: function() {
        console.log('componentDidMount lifecycle method ran!');
        this.interval = setInterval(this.tick, 1000);
    },
    componentWillUnmount: function() {
        console.log('componentWillUnmount lifecycle method ran!');
        clearInterval(this.interval);
    },
    render: function() {
        return (<div>Seconds Elapsed: {this.state.secondsElapsed}</div>);
    }
});

ReactDOM.render(< Timer startTime = "60" / >, app);

组件的生命周期可被分为挂载(Mounting),更新(Updating)和卸载(UnMounting)三个阶段。

下面将对不一样阶段各函数的功能及用途进行描述,弄清这一点很重要
挂载阶段

这是React组件生命周期的第一个阶段,也能够称为组件出生阶段,这个阶段组件被初始化,得到初始的props并定义将会用到的state,此阶段结束时,组件及其子元素都会在UI中被渲染(DOM,UIview等),咱们还能够对渲染后的组件进行进一步的加工。这个阶段的全部方法在组件生命中只会被触发一次。React-in-depth

对挂载阶段的生命周期函数的描述

| 方法 | 描述 |
| --------- | ------ |
|getInitialState()| 在组件挂载前被触发,富状态组件应该调用此方法以得到初始的状态值 |
|componentWillMount()|在组件挂载前被触发,富状态组件应该调用此方法以得到初始的状态值|
|componentDidMount()| 组件被挂载后当即触发,在此能够对DOM进行操做了 |

更新阶段

这个阶段的函数会在组件的整个生命周期中不断被触发,这是组件一辈子中最长的时期。这个阶段的函数能够得到新的props,能够更改state,能够对用户的交互进行反应。React-in-depth

对更新阶段的生命周期函数的描述

方法 描述
componentWillReceiveProps(object nextProps) 在组件接受新的props时被触发,能够用来比较新老props,并使用this.setState()来改变组件状态
shouldComponentUpdate(object nextProps, object nextState) 此组件能够对比新老propsstate,用以确认该组件是否须要从新渲染,若是返回值为false,将跳过这次渲染,此方法经常使用于优化React性能
componentWillUpdate(object nextProps, object nextState) 在组件从新渲染前被触发,此时不能再调用this.setState()state进行更改
componentDidUpdate(object prevProps, object prevState) 在从新渲染后当即被触发,此时可调用新的DOM了

卸载阶段

这是组件生命的最后一个阶段,也能够被称为是组件的死亡阶段,此阶段对应组件从Native UI中卸载之时,具体说来多是用户切换了页面,或者页面改变去除了某个组件,卸载阶段的函数只会被触发一次,而后该组件就会被加入浏览器的垃圾回收机制。React-in-depth

对此阶段的生命周期函数的描述

方法 描述
componentWillUnmount() 组件卸载前当即被触发,此阶段经常使用来执行一些清理工做(好比说清除setInterval

小结

  • componentDidMountcomponentDidUpdate 经常使用来加载第三方的库(此时真实DOM存在,可加载各类图表库)。

  • 组件挂载阶段的各事件执行顺序以下

    1. Initialize / Construction

    2. 获取初始的props,ES5中使用 getDefaultProps() (React.createClass),ES6中使用 MyComponent.defaultProps (ES6 class)

    3. 初始组件的state值,ES5中使用getInitialState() (React.createClass) ,ES6中使用 this.state = ... (ES6 constructor)

    4. componentWillMount()

    5. render()第一次渲染

    6. Children initialization & life cycle kickoff,子组件重复上述(1~5步)过程;

    7. componentDidMount()

经过上面的过程分析,咱们能够知道,在父元素执行componentDidMount()时,子元素和子组件都已经存在于真实DOM中了,所以在此能够放心调用。

  • 组件更新阶段各函数执行顺序以下

    1. componentWillReceiveProps():比较新老props,对state进行改变;

    2. shouldComponentUpdate():判断组件是否须要从新渲染

    3. render():从新渲染

    4. Children Life cycle methods:子元素重复上述过程

    5. componentWillUpdate():此阶段能够调用新的DOM了

  • 组件卸载阶段各函数执行顺序以下

    1. componentWillUnmount()

    2. Children Life cycle methods:触发子元素的生命周期函数,也将被卸载

    3. 被浏览器从内存中清除;

获取子组件和子节点的方法

若是一个组件包含子组件或React节点(如<Parent><Child /></Parent><Parent><span>test<span></Parent>),这些子节点和子组件能够经过React的this.props.children的方法来获取。

下面的例子展现了如何使用this.props.children

var Parent2 = React.createClass({
  componentDidMount: function() {
    //将会得到<span>child2text</span>,
    console.log(this.props.children);
    //将会得到 child2text, 或得了子元素<span>的子元素
    console.log(this.props.children.props.children);
  },

  render: function() {return <div />;}
});

var Parent = React.createClass({
  componentDidMount: function() {
    //得到了一个数组 <div>test</div> <div>test</div>
    console.log(this.props.children);
    //得到了这个数组中的对应子元素中的子元素 childtext,
    console.log(this.props.children[1].props.children);
  },

  render: function() {return <Parent2><span>child2text</span></Parent2>;}
});

ReactDOM.render(
  <Parent><div>child</div><div>childtext</div></Parent>,
  document.getElementById('app')
);

观察上述的代码能够看出如下几点

  • Parent组件实例的this.props.children获取到由直系子元素组成的数组,能够对子元素套用此方法得到子元素(组件)的子元素(组件)(this.props.children[1].props.children);

  • 子元素指的是由该实例围起来的元素,而非该实例内部元素;

为了更好的操做this.props.children包含的是一组元素,React还提供了如下方法

方法 描述
React.Children.map(this.props.children, function(){}) 在每个直接子级(包含在 children 参数中的)上调用 fn 函数,此函数中的 this 指向 上下文。若是 children 是一个内嵌的对象或者数组,它将被遍历,每一个键值对都会添加到新的 Map。若是 children 参数是 null 或者 undefined,那么返回 null 或者 undefined 而不是一个空对象。
React.Children.forEach(this.props.children, function(){}) 相似于Children.map()可是不会反回数组
React.Children.count(this.props.children) 返回组件子元素的总数量,其数目等于Children.map()Children.forEach()的执行次数。
React.Children.only(this.props.children) 返回惟一的子元素不然报错
React.Children.toArray(this.props.children) 返回一个由各子元素组成的数组,若是你想在render事件中操做子元素的集合时,这个方法特别有用,尤为是在从新排序或分割子元素时

小结

  • 当只有一个子元素时,this.props.children之间返回该子元素,不会用一个数组包裹着该子元素;

  • 须要注意的是children并不是某组件内部的节点,而是由该组件包裹的组件或节点‘

两种ref

ref属性使得咱们获取了对某一个React节点或某一个子组件的引用,这个在你须要直接操做DOM时很是有用。

字符串ref的使用很简单,可分为两步:

  • 一是给你想引用的的子元素或组件添加ref属性,

  • 而后在本组件中经过this.refs.value(你所设置的属性名)便可引用;

不过还存在一种函数式的ref,看下面的例子

var C2 = React.createClass({
  render: function() {return <span ref={function(span) {console.log(span)}} />}
});

var C1 = React.createClass({
  render: function() {return(
          <div>
              <C2 ref={function(c2) {console.log(c2)}}></C2>
              <div ref={function(div) {console.log(div)}}></div>
        </div>)}
});

ReactDOM.render(<C1 ref={function(ci) {console.log(ci)}} />,document.getElementById('app'));

上述例子的console结果都是指向ref所在的组件或元素,经过console的结果咱们也能够发现,打印结果说明其指向的是真实的HTML DOM而非Virtual DOM。

若是不想用字符串ref,经过下面的方法也能够引用到你想引用的节点

var MyComponent = React.createClass({
  handleClick: function() {
    // focus()对真实DOM元素有效
      this.textInput.focus();
  },
  render: function() {
    // ref中传入了一个回调函数,把该节点自己赋值给this.input
    return (
      <div>
        <input type="text" ref={(thisInput) => {this.textInput = thisInput}} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.handleClick}
        />
      </div>
    );
  }
});

ReactDOM.render(
  <MyComponent />,
  document.getElementById('app')
);

小结

  • 对无状态函数式组件不能使用ref,由于这种组件并不会返回一个实例;

  • ref有两种,字符串ref和函数式ref,不过字符串ref(经过refs调用这种)在将来可能被放弃,函数式ref是趋势;

  • 组件有ref,能够经过ref调用该组件内部的方法;

  • 使用行内函数表达式使用ref意味着每次更新React都会将其视为一个不一样的函数对象,ref中的函数会以null为参数被当即执行(和在实例中调用不冲突)。好比说,当ref所指向的对象被卸载时,或者ref改变时,老的的ref函数都会以null为参数被调用。

  • 对应ref的使用,React官方有两点建议:

    1. ref容许你直接操做节点,这一点有些状况下是很是方便的,不过须要注意的是,若是能够经过更改state来达到你想要的效果,那就不要随便使用ref啦;

    2. 若是你刚刚接触React,在你想用ref的时候,仍是尽可能多思考一下看能不能用state来解决,仔细思考你会发现,state能够解决大部分操做问题的,比较直接操做DOM并未React的初衷。

从新渲染一个组件

咱们已经接触了ReactDOM.render()方法,这个方法使得组件及其子组件被初始化渲染。在此次渲染以后,React为咱们提供了两种方法来从新渲染某个组件

  1. 在组件内调用setState()方法;

  2. 在组件中调用fouceUpdate()方法;

每当一个组件被从新渲染时,其子组件也会被从新渲染(在Virtual DOM中发生,在真实DOM中表现出来)。不过须要注意的是Virtual DOM的改变并非必定在真实DOM中就会有所表现。

在下面的例子中,ReactDOM.render(< App / >, app)初始化渲染了<App/>及其子组件<Timer/>,接下来的<App/>中的setInterval()事件调用this.setState()导致两个组件被从新渲染。在5秒后,setInterval()被清除,而在十秒后this.forceUpdate()被触发又使得页面被从新渲染。

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

var App = React.createClass({
  getInitialState: function() {
    return {now: Date.now()};
  },

  componentDidMount: function() {
    var foo = setInterval(function() {
        this.setState({now: Date.now()});
    }.bind(this), 1000);

    setTimeout(function(){ clearInterval(foo); }, 5000);
    //DON'T DO THIS, JUST DEMONSTRATING .forceUpdate() 
    setTimeout(function(){ this.state.now = 'foo'; this.forceUpdate() }.bind(this), 10000);
  },
  
  render: function() {
      return (
          <Timer now={this.state.now}></Timer>
        )
    }
});

ReactDOM.render(< App / >, app);

点击JSFiddle查看效果

后文

从开始翻译本书到如今已有一个多月,基础的翻译工做终于算是告一段落。
《React Enlightenment》的第七章和第八章讲述的是React的propsstate已由@linda102翻译完成。

在大概一个多月前看到本书原文时,我已经用了快五个月React,可是看完本书仍是挺有收获。

翻译本书的初衷有两点,一是增强本身对React基础的理解,二是回想起,我在初学React时曾购买过国内的一本关于React的基础书籍,价格是四十多,可是其实看完并未有太多收获,该书大多就是翻译的官方文档,并且翻译的也不全面,并不那么容易理解,因此但愿这篇译文对初学者友好,让初学者少走弯路。

因为翻译时间和水平都有限,译文内部不可避免存在一些不恰当的地方,若是您在阅读的过程当中有好的建议,请直接提出,我会尽快修改。谢谢

一些有用的连接

本书全文在Gitbook中观看

本文英文原文

相关文章
相关标签/搜索