(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中觀看

本文英文原文

相關文章
相關標籤/搜索