React 組件

React 組件

一個 React 應用就是構建在React組件之上的。react

組件有兩個核心概念:數組

  • propspromise

  • state瀏覽器

一個組件就是經過這兩個屬性的值在 render 方法裏面生成這個組件對應的 HTML 結構。緩存

注意:組件生成的 HTML 結構只能有一個單一的根節點。架構

propsapp

props 就是組件的屬性,由外部經過 JSX 屬性傳入設置,一旦初始設置完成,就能夠認爲 this.props 是不可更改的,因此不要輕易更改設置 this.props 裏面的值(雖然對於一個 JS 對象你能夠作任何事)。less

statedom

state 是組件的當前狀態,能夠把組件簡單當作一個「狀態機」,根據狀態 state 呈現不一樣的 UI 展現。ide

一旦狀態(數據)更改,組件就會自動調用 render 從新渲染 UI,這個更改的動做會經過 this.setState 方法來觸發。

劃分狀態數據

一條原則:讓組件儘量地少狀態。

這樣組件邏輯就越容易維護。

什麼樣的數據屬性能夠看成狀態?

當更改這個狀態(數據)須要更新組件 UI 的就能夠認爲是 state,下面這些能夠認爲不是狀態:

  • 可計算的數據:好比一個數組的長度

  • props 重複的數據:除非這個數據是要作變動的

無狀態組件

你也能夠用純粹的函數來定義無狀態的組件(stateless function)這種組件沒有狀態,沒有生命週期,只是簡單的接受 props 渲染生成 DOM 結構。無狀態組件很是簡單,開銷很低,若是可能的話儘可能使用無狀態組件。好比使用箭頭函數定義:

const HelloMessage = (props) => <div>Hello {props.name}</div>;
render(<HelloMessage name="Jim"/>, app);

由於無狀態組件只是函數,因此它沒有實例返回,這點在想用 refs 獲取無狀態組件的時候要注意。

組件生命週期

通常來講,一個組件類由 extends Component 建立,而且提供一個 render 方法以及其餘可選的生命週期函數組件相關的事件或方法來定義。

getInitialState

初始化 this.state 的值,只在組件裝載以前調用一次。

若是是使用 ES6 的語法,你也能夠在構造函數中初始化狀態,好比:

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {count: props.initialCount};
    }

    render() {
        
    }
}

getDefaultProps

只在組件建立時調用一次並緩存返回的對象(即在 React.createClass 以後就會調用)。

由於這個方法在實例初始化以前調用,因此在這個方法裏面不能依賴 this 獲取到這個組件的實例。

在組件裝載以後,這個方法緩存的結果會用來保證訪問 this.props 的屬性時,當這個屬性沒有在父組件中傳入(在這個組件的 JSX 屬性裏設置),也老是有值的。

若是是使用 ES6 語法,能夠直接定義 defaultProps 這個類屬性來替代,這樣能更直觀的知道 default props 是預先定義好的對象值:

Counter.defaultProps = {initialCount: 0};

render

組裝生成這個組件的 HTML 結構(使用原生 HTML 標籤或者子組件),也能夠返回 null 或者 false,這時候 ReactDOM.findDOMNode(this) 會返回 null

生命週期函數

裝載組件觸發

componentWillMount

只會在裝載以前調用一次,在 render 以前調用,你能夠在這個方法裏面調用 setState 改變狀態,而且不會致使額外調用一次 render

componentDidMount

只會在裝載完成以後調用一次,在 render 以後調用,從這裏開始能夠經過 ReactDOM.findDOMNode(this) 獲取到組件的 DOM 節點。

更新組件觸發

這些方法不會在首次 render 組件的週期調用

  • componentWillReceiveProps

  • shouldComponentUpdate

  • componentWillUpdate

  • componentDidUpdate

卸載組件觸發

  • componentWillUnmount

事件處理

能夠看到 React 裏面綁定事件的方式和在 HTML 中綁定事件相似,使用駝峯式命名指定要綁定的 onClick 屬性爲組件定義的一個方法 {this.handleClick.bind(this)}

注意要顯式調用 bind(this) 將事件函數上下文綁定要組件實例上,這也是 React 推崇的原則:沒有黑科技,儘可能使用顯式的容易理解的 JavaScript 代碼。

「合成事件」和「原生事件」

React 實現了一個「合成事件」層(synthetic event system),這個事件模型保證了和 W3C 標準保持一致,因此不用擔憂有什麼詭異的用法,而且這個事件層消除了 IEW3C 標準實現之間的兼容問題。

「合成事件」還提供了額外的好處:

事件委託

「合成事件」會以事件委託event delegation)的方式綁定到組件最上層,而且在組件卸載unmount)的時候自動銷燬綁定的事件

什麼是「原生事件」?

好比你在 componentDidMount 方法裏面經過 addEventListener 綁定的事件就是瀏覽器原生事件。

使用原生事件的時候注意在 componentWillUnmount 解除綁定 removeEventListener

全部經過 JSX 這種方式綁定的事件都是綁定到「合成事件」,除非你有特別的理由,建議老是用 React 的方式處理事件。

Tips

若是混用「合成事件」和「原生事件」,好比一種常見的場景是用原生事件在 document 上綁定,而後在組件裏面綁定的合成事件想要經過 e.stopPropagation() 來阻止事件冒泡到 document,這時候是行不通的,參見 Event delegation,由於 e.stopPropagation 是內部「合成事件」 層面的,解決方法是要用 e.nativeEvent.stopImmediatePropagation()

「合成事件」 的 event 對象只在當前 event loop 有效,好比你想在事件裏面調用一個 promise,在 resolve 以後去拿 event 對象會拿不到(而且沒有錯誤拋出):

handleClick(e) {
  promise.then(() => doSomethingWith(e));
}

參數傳遞

給事件處理函數傳遞額外參數的方式:bind(this, arg1, arg2, ...)

render: function() {
    return <p onClick={this.handleClick.bind(this, 'extra param')}>;
},
handleClick: function(param, event) {
    // handle click
}

DOM 操做

大部分狀況下你不須要經過查詢 DOM 元素去更新組件的 UI,你只要關注設置組件的狀態(setState)。可是可能在某些狀況下你確實須要直接操做 DOM

首先咱們要了解 ReactDOM.render 組件返回的是什麼?

它會返回對組件的引用也就是組件實例(對於無狀態狀態組件來講返回 null),注意 JSX 返回的不是組件實例,它只是一個 ReactElement 對象(還記得咱們用純 JS 來構建 JSX 的方式嗎),好比這種:

// A ReactElement
const myComponent = <MyComponent />

// render
const myComponentInstance = ReactDOM.render(myComponent, mountNode);
myComponentInstance.doSomething();

findDOMNode()

當組件加載到頁面上以後(mounted),你均可以經過 react-dom 提供的 findDOMNode() 方法拿到組件對應的 DOM 元素。

import { findDOMNode } from 'react-dom';

// Inside Component class
componentDidMound() {
  const el = findDOMNode(this);
}

findDOMNode() 不能用在無狀態組件上。

Refs

另一種方式就是經過在要引用的 DOM 元素上面設置一個 ref 屬性指定一個名稱,而後經過 this.refs.name 來訪問對應的 DOM 元素。

好比有一種狀況是必須直接操做 DOM 來實現的,你但願一個 <input/> 元素在你清空它的值時 focus,你無法僅僅靠 state 來實現這個功能。

class App extends Component {
  constructor() {
    return { userInput: '' };
  }

  handleChange(e) {
    this.setState({ userInput: e.target.value });
  }

  clearAndFocusInput() {
    this.setState({ userInput: '' }, () => {
      this.refs.theInput.focus();
    });
  }

  render() {
    return (
      <div>
        <div onClick={this.clearAndFocusInput.bind(this)}>
          Click to Focus and Reset
        </div>
        <input
          ref="theInput"
          value={this.state.userInput}
          onChange={this.handleChange.bind(this)}
        />
      </div>
    );
  }
}

若是 ref 是設置在原生 HTML 元素上,它拿到的就是 DOM 元素,若是設置在自定義組件上,它拿到的就是組件實例,這時候就須要經過 findDOMNode 來拿到組件的 DOM 元素。

由於無狀態組件沒有實例,因此 ref 不能設置在無狀態組件上,通常來講這沒什麼問題,由於無狀態組件沒有實例方法,不須要 ref 去拿實例調用相關的方法,可是若是想要拿無狀態組件的 DOM 元素的時候,就須要用一個狀態組件封裝一層,而後經過 reffindDOMNode 去獲取。

總結

  • 你可使用 ref 到的組件定義的任何公共方法,好比 this.refs.myTypeahead.reset()

  • Refs 是訪問到組件內部 DOM 節點惟一可靠的方法

注意事項

  • 不要在 render 或者 render 以前訪問 refs

  • 不要濫用 refs,好比只是用它來按照傳統的方式操做界面 UI:找到 DOM -> 更新 DOM

組合組件

使用組件的目的就是經過構建模塊化的組件,相互組合組件最後組裝成一個複雜的應用。

React 組件中要包含其餘組件做爲子組件,只須要把組件看成一個 DOM 元素引入就能夠了。

循環插入子元素

若是組件中包含經過循環插入的子元素,爲了保證從新渲染 UI 的時候可以正確顯示這些子元素,每一個元素都須要經過一個特殊的 key 屬性指定一個惟一值。

key 必須直接在循環中設置:

const MyComponent = (props) => {
    return (
        <ul>
            {props.results.map((result) => {
                return <ListItemWrapper key={result.id} data={result}/>;
            })}
        </ul>
    );
};

你也能夠用一個 key 值做爲屬性,子元素做爲屬性值的對象字面量來顯示子元素列表。

實際上瀏覽器在遍歷一個字面量對象的時候會保持順序一致,除非存在屬性值能夠被轉換成整數值,這種屬性值會排序並放在其餘屬性以前被遍歷到,因此爲了防止這種狀況發生,能夠在構建這個字面量的時候在 key 值前面加字符串前綴。

HTML 元素會做爲 React 組件對象、JS 表達式結果是一個文字節點,都會存入 Parent 組件的 props.children

通常來講,能夠直接將這個屬性做爲父組件的子元素 render

const Parent = (props) => <div>{props.children}</div>;

props.children 一般是一個組件對象的數組,可是當只有一個子元素的時候,props.children 將是這個惟一的子元素,而不是數組了。

React.Children 提供了額外的方法方便操做這個屬性。

組件間通訊

父子組件間通訊

這種狀況下很簡單,就是經過 props 屬性傳遞,在父組件給子組件設置 props,而後子組件就能夠經過 props 訪問到父組件的數據/方法,這樣就搭建起了父子組件間通訊的橋樑。

div 能夠看做一個子組件,指定它的 onClick 事件調用父組件的方法。

import React, {Component} from 'react';
import {render} from 'react-dom';

class GroceryList extends Component {
    handleClick(i) {
        console.log('You clicked: ' + this.props.items[i]);
    }

    render() {
        return (
            <ul>
                {this.props.items.map((item, i) => {
                    return (
                        <li onClick={this.handleClick.bind(this, i)} key={i}>{item}</li>
                    )
                })}
            </ul>
        )
    }
}
render(
    <GroceryList items={['Apple', 'Banana', 'Cranberry']}/>,
    mountNode
);

div 能夠看做一個子組件,指定它的 onClick 事件調用父組件的方法。

非父子組件間的通訊

使用全局事件 Pub/Sub 模式,在 componentDidMount 裏面訂閱事件,在 componentWillUnmount 裏面取消訂閱,當收到事件觸發的時候調用 setState 更新 UI

這種模式在複雜的系統裏面可能會變得難以維護,因此看我的權衡是否將組件封裝到大的組件,甚至整個頁面或者應用就封裝到一個組件。

通常來講,對於比較複雜的應用,推薦使用相似 Flux 這種單項數據流架構。

相關文章
相關標籤/搜索