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
}
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儒溉。