(React啟蒙)理解React 元件

zhangwang發表於2016-09-16

本文將首先講述如何通過React nodes建立基礎的React元件,然後進一步剖析React元件內部的點滴,包括該如何理解React元件,獲取React元件例項的兩種辦法,React事件系統,對React生命週期函式的理解,獲取React元件的子元件和子節點的方法,字串ref和函式式ref,以及觸發React元件重新渲染的四種方法。
本文是React啟蒙系列的第六章,依舊講的是React的基礎使用方法,但是如果你對上面提到的概念有不理解或不熟悉的地方,跳到對應地方觀看閱讀,你應該會能有所收穫。

理解React元件

在具體說明如何建立React元件的語法之前,對什麼是React元件,其存在的意思及其劃分依據等做一個論述是很有必要的。

我們設想現在有一個webApp,這個app可以用來實現很多功能,依據功能,我們可以把其劃分為多個功能碎片。要實現這麼一個功能碎片,可能需要更多更小的邏輯單元,甚至還可以繼續分。而我們程式設計其實就是在有一個總體輪廓的前提下,通過解決一個個小小的問題來解決一個小問題,解決一個個小問題來實現軟體的開發。React元件就是這樣,你可以就把它當做一個個可組合的功能單元。

以一個登陸框為例,登入框本身就是網站的一個元件,但是其內包含諸如文字輸入框,登陸按鈕等,當然如果你想要做的只是最基礎的功能,輸入框和按鈕等可以只是一個個React 節點,但是如果你想為輸入框加上輸入檢測,輸入框可能就有必要寫成一個單獨的元件了,這樣也有利於複用,之後需要做的可能只是簡單的通過props傳入不同的引數就可以實現不同的檢測。假想我們現在的登入框元件,包含React <Button>元素形成登入按鈕,也包含多個文字輸入檢測元件。那麼父元件的作用一方面在於聚合小元件形成更復雜的功能單元,另一方面在於為子元件資訊的溝通提供渠道(比如說在滿足一定的輸入條件後,登入按鈕的狀態從不可點選變為可點選)。

建立React元件

React元件通過呼叫React.createClass()方法建立,該方法需要傳入一個物件形式的引數。在該物件中可以為所建立元件配置各種引數,其可用引數如下表:

方法(配置引數)名稱 描述
render() 必填,通常為一個返回React nodes或者其它元件的函式
getInitialState() 一個用於設定最初的state的函式,返回一個物件
getDefaultProps() 一個用於設定預設props的函式,返回值為一個物件
propTypes 一個用於驗證特定props型別的物件
mixins 元件間共享方法的途徑
statics 一個由多個靜態方法組成的物件,靜態方法中不能直接呼叫propsstate(可通過引數)
displayName 是一個用於命名元件的字串,用於展示除錯資訊,使用JSX時將自動設定??
componentWillMount() 在元件首次渲染前觸發,只會觸發一次
componentDidMount() 在元件首次渲染後觸發,只會觸發一次
componentWillReceiveProps() 在元件將接受新props時觸發
shouldComponentUpdate() 元件再次渲染前觸發,可用於判斷是否需要再次渲染
componentWillUpdate() 元件再次渲染前立即觸發
componentDidUpdate() 元件渲染後立即觸發
componentWillUnmount() 元件解除安裝前立即觸發

在上述所以方法中,最重要且必不可少的是render(),它的作用是返回React節點和元件,其它所有的方法是可選的。

實際寫一個例子總比空說要容易理解,以下是使用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元件中事件的定義等等。

在此需要注意的是元件名是以大寫開頭的。

當一個元件被建立(掛載)以後,我們就可以使用元件的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中觀看

本文英文原文

相關文章