3.組件

React 組件

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

組件有兩個(gè)核心概念:

  • props
  • state

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

props

前面也提到很多次了吠昭,props 就是組件的屬性客扎,由外部通過(guò) JSX 屬性傳入設(shè)置英上,一旦初始設(shè)置完成炭序,就可以認(rèn)為this.props是不可更改的啤覆,所以不要輕易更改設(shè)置this.props里面的值(雖然對(duì)于一個(gè) JS 對(duì)象你可以做任何事)。

state

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

一旦狀態(tài)(數(shù)據(jù))更改辜纲,組件就會(huì)自動(dòng)調(diào)用 render 重新渲染 UI笨觅,這個(gè)更改的動(dòng)作會(huì)通過(guò) this.setState 方法來(lái)觸發(fā)。

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

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

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

最后回過(guò)頭來(lái)反復(fù)看幾遍 Thinking inReact苍苞,相信會(huì)對(duì)組件有更深刻的認(rèn)識(shí)。

無(wú)狀態(tài)組件

你也可以用純粹的函數(shù)來(lái)定義無(wú)狀態(tài)的組件(stateless function)狼纬,這種組件沒(méi)有狀態(tài)羹呵,沒(méi)有生命周期,只是簡(jiǎn)單的接受 props 渲染生成 DOM 結(jié)構(gòu)疗琉。無(wú)狀態(tài)組件非常簡(jiǎn)單冈欢,開(kāi)銷很低,如果可能的話盡量使用無(wú)狀態(tài)組件没炒。比如使用箭頭函數(shù)定義:

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

因?yàn)闊o(wú)狀態(tài)組件只是函數(shù)涛癌,所以它沒(méi)有實(shí)例返回,這點(diǎn)在想用 refs獲取無(wú)狀態(tài)組件的時(shí)候要注意送火,參見(jiàn)DOM 操作拳话。

組件生命周期

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

一個(gè)簡(jiǎ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 的語(yǔ)法,你也可以在構(gòu)造函數(shù)中初始化狀態(tài)猖败,比如:

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

  render() {
    // ...
  }
}

getDefaultProps

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

因?yàn)檫@個(gè)方法在實(shí)例初始化之前調(diào)用,所以在這個(gè)方法里面不能依賴 this 獲取到這個(gè)組件的實(shí)例恩闻。

在組件裝載之后艺糜,這個(gè)方法緩存的結(jié)果會(huì)用來(lái)保證訪問(wèn) this.props 的屬性時(shí),當(dāng)這個(gè)屬性沒(méi)有在父組件中傳入(在這個(gè)組件的 JSX 屬性里設(shè)置),也總是有值的破停。

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

Counter.defaultProps = { initialCount: 0 };

render

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

生命周期函數(shù)

裝載組件觸發(fā)

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

componentDidMount
只會(huì)在裝載完成之后調(diào)用一次园爷,在 render 之后調(diào)用宠蚂,從這里開(kāi)始可以通過(guò) ReactDOM.findDOMNode(this) 獲取到組件的 DOM 節(jié)點(diǎn)。

更新組件觸發(fā)

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

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate

卸載組件觸發(fā)

  • componentWillUnmount

更多關(guān)于組件相關(guān)的方法說(shuō)明童社,參見(jiàn):

事件處理

一個(gè)簡(jiǎ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屬性為組件定義的一個(gè)方法 {this.handleClick.bind(this)} 著隆。
注意要顯式調(diào)用 bind(this) 將事件函數(shù)上下文綁定要組件實(shí)例上扰楼,這也是 React 推崇的原則:沒(méi)有黑科技,盡量使用顯式的容易理解的 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 操作

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

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

它會(huì)返回對(duì)組件的引用也就是組件實(shí)例(對(duì)于無(wú)狀態(tài)狀態(tài)組件來(lái)說(shuō)返回 null),注意 JSX 返回的不是組件實(shí)例流酬,它只是一個(gè) ReactElement 對(duì)象(還記得我們用純 JS 來(lái)構(gòu)建 JSX 的方式嗎)币厕,比如這種:

// A ReactElement
const myComponent = <MyComponent />

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

findDOMNode()

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

import { findDOMNode } from 'react-dom';

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

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

Refs

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

比如有一種情況是必須直接操作 DOM 來(lái)實(shí)現(xiàn)的阴绢,你希望一個(gè) <input/> 元素在你清空它的值時(shí) focus,你沒(méi)法僅僅靠 state 來(lái)實(shí)現(xiàn)這個(gè)功能艰躺。

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è)置在自定義組件上腺兴,它拿到的就是組件實(shí)例左电,這時(shí)候就需要通過(guò) findDOMNode 來(lái)拿到組件的 DOM 元素。

因?yàn)闊o(wú)狀態(tài)組件沒(méi)有實(shí)例,所以 ref 不能設(shè)置在無(wú)狀態(tài)組件上券腔,一般來(lái)說(shuō)這沒(méi)什么問(wèn)題伏穆,因?yàn)闊o(wú)狀態(tài)組件沒(méi)有實(shí)例方法,不需要 ref 去拿實(shí)例調(diào)用相關(guān)的方法纷纫,但是如果想要拿無(wú)狀態(tài)組件的 DOM 元素的時(shí)候枕扫,就需要用一個(gè)狀態(tài)組件封裝一層,然后通過(guò) ref 和 findDOMNode 去獲取辱魁。

總結(jié)

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

注意事項(xiàng)

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

組合組件

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

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

一個(gè)例子:一個(gè)顯示用戶頭像的組件 Avatar 包含兩個(gè)子組件 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')
);

通過(guò) props 傳遞值砾赔。

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

如果組件中包含通過(guò)循環(huán)插入的子元素,為了保證重新渲染 UI的時(shí)候能夠正確顯示這些子元素青灼,每個(gè)元素都需要通過(guò)一個(gè)特殊的key屬性指定一個(gè)唯一值暴心。具體原因見(jiàn)這里,為了內(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>
  );
}

你也可以用一個(gè)key值作為屬性专普,子元素作為屬性值的對(duì)象字面量來(lái)顯示子元素列表,雖然這種用法的場(chǎng)景有限弹沽,參見(jiàn)Keyed Fragments檀夹,但是在這種情況下要注意生成的子元素重新渲染后在 DOM 中顯示的順序問(wèn)題。

實(shí)際上瀏覽器在遍歷一個(gè)字面量對(duì)象的時(shí)候會(huì)保持順序一致策橘,除非存在屬性值可以被轉(zhuǎn)換成整數(shù)值炸渡,這種屬性值會(huì)排序并放在其他屬性之前被遍歷到偶摔,所以為了防止這種情況發(fā)生辰斋,可以在構(gòu)建這個(gè)字面量的時(shí)候在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>
   );
}

組件間通信

父子組件間通信

這種情況下很簡(jiǎn)單孽糖,就是通過(guò) props 屬性傳遞,在父組件給子組件設(shè)置 props病蛉,然后子組件就可以通過(guò) props 訪問(wèn)到父組件的數(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 可以看作一個(gè)子組件,指定它的 onClick 事件調(diào)用父組件的方法。
父組件訪問(wèn)子組件?用 refs

非父子組件間的通信

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涛碑,一起剝皮案震驚了整個(gè)濱河市瘫证,隨后出現(xiàn)的幾起案子余黎,更是在濱河造成了極大的恐慌巡扇,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門检盼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)吨枉,“玉大人忠烛,你說(shuō)我怎么就攤上這事∈玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵胯陋,是天一觀的道長(zhǎng)蕊温。 經(jīng)常有香客問(wèn)我袱箱,道長(zhǎng),這世上最難降的妖魔是什么义矛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任发笔,我火速辦了婚禮,結(jié)果婚禮上凉翻,老公的妹妹穿的比我還像新娘了讨。我一直安慰自己,他們只是感情好制轰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布前计。 她就那樣靜靜地躺著,像睡著了一般垃杖。 火紅的嫁衣襯著肌膚如雪男杈。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天缩滨,我揣著相機(jī)與錄音势就,去河邊找鬼。 笑死脉漏,一個(gè)胖子當(dāng)著我的面吹牛苞冯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侧巨,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼舅锄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了司忱?” 一聲冷哼從身側(cè)響起皇忿,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坦仍,沒(méi)想到半個(gè)月后鳍烁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡繁扎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年幔荒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梳玫。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爹梁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出提澎,到底是詐尸還是另有隱情姚垃,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布盼忌,位于F島的核電站积糯,受9級(jí)特大地震影響掂墓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜絮宁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一梆暮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绍昂,春花似錦啦粹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至忍饰,卻和暖如春贪嫂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背艾蓝。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工力崇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赢织。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓亮靴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親于置。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茧吊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345