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