3.組件

React 組件

可以這么說,一個 React 應(yīng)用就是構(gòu)建在 React 組件之上的。

組件有兩個核心概念:

  • props
  • state

一個組件就是通過這兩個屬性的值在render方法里面生成這個組件對應(yīng)的 HTML 結(jié)構(gòu)。
注意:組件生成的 HTML 結(jié)構(gòu)只能有一個單一的根節(jié)點。

props

前面也提到很多次了,props 就是組件的屬性,由外部通過 JSX 屬性傳入設(shè)置,一旦初始設(shè)置完成,就可以認為this.props是不可更改的,所以不要輕易更改設(shè)置this.props里面的值(雖然對于一個 JS 對象你可以做任何事)。

state

state 是組件的當(dāng)前狀態(tài),可以把組件簡單看成一個“狀態(tài)機”,根據(jù)狀態(tài) state 呈現(xiàn)不同的 UI 展示。

一旦狀態(tài)(數(shù)據(jù))更改,組件就會自動調(diào)用 render 重新渲染 UI,這個更改的動作會通過 this.setState 方法來觸發(fā)。

劃分狀態(tài)數(shù)據(jù)

一條原則:讓組件盡可能地少狀態(tài)。
這樣組件邏輯就越容易維護。
什么樣的數(shù)據(jù)屬性可以當(dāng)作狀態(tài)?
當(dāng)更改這個狀態(tài)(數(shù)據(jù))需要更新組件 UI 的就可以認為是state
,下面這些可以認為不是狀態(tài):

  • 可計算的數(shù)據(jù):比如一個數(shù)組的長度
  • 和 props 重復(fù)的數(shù)據(jù):除非這個數(shù)據(jù)是要做變更的

最后回過頭來反復(fù)看幾遍 Thinking inReact,相信會對組件有更深刻的認識。

無狀態(tài)組件

你也可以用純粹的函數(shù)來定義無狀態(tài)的組件(stateless function),這種組件沒有狀態(tài),沒有生命周期,只是簡單的接受 props 渲染生成 DOM 結(jié)構(gòu)。無狀態(tài)組件非常簡單,開銷很低,如果可能的話盡量使用無狀態(tài)組件。比如使用箭頭函數(shù)定義:

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

因為無狀態(tài)組件只是函數(shù),所以它沒有實例返回,這點在想用 refs獲取無狀態(tài)組件的時候要注意,參見DOM 操作。

組件生命周期

一般來說,一個組件類由 extends Component 創(chuàng)建,并且提供一個 render 方法以及其他可選的生命周期函數(shù)、組件相關(guān)的事件或方法來定義。

一個簡單的例子:

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

class LikeButton extends Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  handleClick(e) {
    this.setState({ liked: !this.state.liked });
  }

  render() {
    const text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick.bind(this)}>
          You {text} this. Click to toggle.
      </p>
    );
  }
}

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

getInitialState

初始化 this.state 的值,只在組件裝載之前調(diào)用一次。

如果是使用 ES6 的語法,你也可以在構(gòu)造函數(shù)中初始化狀態(tài),比如:

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

  render() {
    // ...
  }
}

getDefaultProps

只在組件創(chuàng)建時調(diào)用一次并緩存返回的對象(即在 React.createClass 之后就會調(diào)用)。

因為這個方法在實例初始化之前調(diào)用,所以在這個方法里面不能依賴 this 獲取到這個組件的實例。

在組件裝載之后,這個方法緩存的結(jié)果會用來保證訪問 this.props 的屬性時,當(dāng)這個屬性沒有在父組件中傳入(在這個組件的 JSX 屬性里設(shè)置),也總是有值的。

如果是使用 ES6 語法,可以直接定義 defaultProps 這個類屬性來替代,這樣能更直觀的知道 default props 是預(yù)先定義好的對象值:

Counter.defaultProps = { initialCount: 0 };

render

必須(required)
組裝生成這個組件的 HTML 結(jié)構(gòu)(使用原生 HTML 標(biāo)簽或者子組件),也可以返回 null 或者 false ,這時候 ReactDOM.findDOMNode(this) 會返回 null 。

生命周期函數(shù)

裝載組件觸發(fā)

componentWillMount
只會在裝載之前調(diào)用一次,在 render 之前調(diào)用,你可以在這個方法里面調(diào)用 setState 改變狀態(tài),并且不會導(dǎo)致額外調(diào)用一次 render

componentDidMount
只會在裝載完成之后調(diào)用一次,在 render 之后調(diào)用,從這里開始可以通過 ReactDOM.findDOMNode(this) 獲取到組件的 DOM 節(jié)點。

更新組件觸發(fā)

這些方法不會在首次 render 組件的周期調(diào)

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate

卸載組件觸發(fā)

  • componentWillUnmount

更多關(guān)于組件相關(guān)的方法說明,參見:

事件處理

一個簡單的例子:

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

class LikeButton extends Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  handleClick(e) {
    this.setState({ liked: !this.state.liked });
  }

  render() {
    const text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick.bind(this)}>
          You {text} this. Click to toggle.
      </p>
    );
  }
}

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

可以看到 React 里面綁定事件的方式和在 HTML 中綁定事件類似,使用駝峰式命名指定要綁定的 onClick屬性為組件定義的一個方法 {this.handleClick.bind(this)} 。
注意要顯式調(diào)用 bind(this) 將事件函數(shù)上下文綁定要組件實例上,這也是 React 推崇的原則:沒有黑科技,盡量使用顯式的容易理解的 JavaScript 代碼。

參數(shù)傳遞

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

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

React 支持的事件列表

DOM 操作

大部分情況下你不需要通過查詢 DOM 元素去更新組件的 UI,你只要關(guān)注設(shè)置組件的狀態(tài)(setState)。但是可能在某些情況下你確實需要直接操作 DOM。

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

它會返回對組件的引用也就是組件實例(對于無狀態(tài)狀態(tài)組件來說返回 null),注意 JSX 返回的不是組件實例,它只是一個 ReactElement 對象(還記得我們用純 JS 來構(gòu)建 JSX 的方式嗎),比如這種:

// A ReactElement
const myComponent = <MyComponent />

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

findDOMNode()

當(dāng)組件加載到頁面上之后(mounted),你都可以通過 react-dom 提供的 findDOMNode() 方法拿到組件對應(yīng)的 DOM 元素。

import { findDOMNode } from 'react-dom';

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

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

Refs

另外一種方式就是通過在要引用的 DOM 元素上面設(shè)置一個 ref 屬性指定一個名稱,然后通過 this.refs.name 來訪問對應(yīng)的 DOM 元素。

比如有一種情況是必須直接操作 DOM 來實現(xiàn)的,你希望一個 <input/> 元素在你清空它的值時 focus,你沒法僅僅靠 state 來實現(xiàn)這個功能。

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 是設(shè)置在原生 HTML 元素上,它拿到的就是 DOM 元素,如果設(shè)置在自定義組件上,它拿到的就是組件實例,這時候就需要通過 findDOMNode 來拿到組件的 DOM 元素。

因為無狀態(tài)組件沒有實例,所以 ref 不能設(shè)置在無狀態(tài)組件上,一般來說這沒什么問題,因為無狀態(tài)組件沒有實例方法,不需要 ref 去拿實例調(diào)用相關(guān)的方法,但是如果想要拿無狀態(tài)組件的 DOM 元素的時候,就需要用一個狀態(tài)組件封裝一層,然后通過 ref 和 findDOMNode 去獲取。

總結(jié)

  • 你可以使用 ref 到的組件定義的任何公共方法,比如this.refs.myTypeahead.reset()
  • Refs 是訪問到組件內(nèi)部 DOM 節(jié)點唯一可靠的方法
  • Refs 會自動銷毀對子組件的引用(當(dāng)子組件刪除時)

注意事項

  • 不要在 render 或者 render 之前訪問 refs
  • 不要濫用 refs,比如只是用它來按照傳統(tǒng)的方式操作界面 UI:找到 DOM -> 更新 DOM

組合組件

使用組件的目的就是通過構(gòu)建模塊化的組件,相互組合組件最后組裝成一個復(fù)雜的應(yīng)用。

在 React 組件中要包含其他組件作為子組件,只需要把組件當(dāng)作一個 DOM 元素引入就可以了。

一個例子:一個顯示用戶頭像的組件 Avatar 包含兩個子組件 ProfilePic 顯示用戶頭像和 ProfileLink 顯示用戶鏈接:

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

const ProfilePic = (props) => {
  return (
    <img src={'http://graph.facebook.com/' + props.username + '/picture'} />
  );
}

const ProfileLink = (props) => {
  return (
    <a href={'http://www.facebook.com/' + props.username}>
      {props.username}
    </a>
  );
}

const Avatar = (props) => {
  return (
    <div>
      <ProfilePic username={props.username} />
      <ProfileLink username={props.username} />
    </div>
  );
}

render(
  <Avatar username="pwh" />,
  document.getElementById('example')
);

通過 props 傳遞值。

循環(huán)插入子元素

如果組件中包含通過循環(huán)插入的子元素,為了保證重新渲染 UI的時候能夠正確顯示這些子元素,每個元素都需要通過一個特殊的key屬性指定一個唯一值。具體原因見這里,為了內(nèi)部 diff 的效率。
key 必須直接在循環(huán)中設(shè)置:

const ListItemWrapper = (props) => <li>{props.data.text}</li>;

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

你也可以用一個key值作為屬性,子元素作為屬性值的對象字面量來顯示子元素列表,雖然這種用法的場景有限,參見Keyed Fragments,但是在這種情況下要注意生成的子元素重新渲染后在 DOM 中顯示的順序問題。

實際上瀏覽器在遍歷一個字面量對象的時候會保持順序一致,除非存在屬性值可以被轉(zhuǎn)換成整數(shù)值,這種屬性值會排序并放在其他屬性之前被遍歷到,所以為了防止這種情況發(fā)生,可以在構(gòu)建這個字面量的時候在key值前面加字符串前綴,比如:

render() {
  var items = {};

  this.props.results.forEach((result) => {
    // If result.id can look like a number (consider short hashes), then
    // object iteration order is not guaranteed. In this case, we add a prefix
    // to ensure the keys are strings.
    items['result-' + result.id] = <li>{result.text}</li>;
  });

  return (
    <ol>
      {items}
    </ol>
   );
}

組件間通信

父子組件間通信

這種情況下很簡單,就是通過 props 屬性傳遞,在父組件給子組件設(shè)置 props,然后子組件就可以通過 props 訪問到父組件的數(shù)據(jù)/方法,這樣就搭建起了父子組件間通信的橋梁。

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 (
      <div>
        {this.props.items.map((item, i) => {
          return (
            <div onClick={this.handleClick.bind(this, i)} key={i}>{item}</div>
          );
        })}
      </div>
    );
  }
}

render(
  <GroceryList items={['Apple', 'Banana', 'Cranberry']} />, mountNode
);

div 可以看作一個子組件,指定它的 onClick 事件調(diào)用父組件的方法。
父組件訪問子組件?用 refs

非父子組件間的通信

使用全局事件 Pub/Sub 模式,在 componentDidMount 里面訂閱事件,在 componentWillUnmount 里面取消訂閱,當(dāng)收到事件觸發(fā)的時候調(diào)用setState更新UI。
這種模式在復(fù)雜的系統(tǒng)里面可能會變得難以維護,所以看個人權(quán)衡是否將組件封裝到大的組件,甚至整個頁面或者應(yīng)用就封裝到一個組件。
一般來說,對于比較復(fù)雜的應(yīng)用,推薦使用類似 Flux 這種單項數(shù)據(jù)流架構(gòu),參見DataFlow。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。