剛入門React可能會因爲React的單向數據流的特性而遇到組件間溝通的麻煩,這篇文章主要就說一說如何解決組件間溝通的問題。
1.組件間的關係
1.1 父子組件
ReactJS中數據的流動是單向的,父組件的數據可以通過設置子組件的props傳遞數據給子組件。如果想讓子組件改變父組件的數據,可以在父組件中傳一個callback(回調函數)給子組件,子組件內調用這個callback即可改變父組件的數據。
var MyContainer = React.createClass({
getInitialState: function(){
return {
list: ['item1', 'item2'],
curItem: 'item1'
}
},
// 改變curItem的回調函數
changeItem: function(item){
this.setState({
curItem: item
});
},
render: function(){
return (
<div>
The curItem is: {this.state.curItem}
<List list={this.state.list} changeItem={this.changeItem}/>
</div>
)
}
});
var List = React.createClass({
onClickItem: function(item){
this.props.changeItem(item);
},
render: function(){
return (
<ul>
{
(function(){
var self = this;
return this.props.list.map(function(item){
return (
<li onClick={self.onClickItem.bind(self, item)}>I am {item}, click me!</li>
)
});
}.bind(this))()
}
</ul>
)
}
})
ReactDOM.render(
<MyContainer />,
document.getElementById('example')
);
<MyContainer />是<List />的父組件,<MyContainer />通過props傳遞list數據給<List />組件,如果<MyContainer />中的list改變,<List />會重新渲染列表數據。而<List />可以通過<MyContainer />傳來的changeItem函數,改變<MyContainer />的curItem數據。
1.2 兄弟組件
當兩個組件不是父子關係,但有相同的父組件時,將這兩個組件稱爲兄弟組件。兄弟組件不能直接相互傳送數據,此時可以將數據掛載在父組件中,由兩個組件共享:如果組件需要數據渲染,則由父組件通過props傳遞給該組件;如果組件需要改變數據,則父組件傳遞一個改變數據的回調函數給該組件,並在對應事件中調用。
var MyContainer = React.createClass({
getInitialState: function(){
return {
list: ['item1', 'item2'],
curItem: 'item1'
}
},
// 改變curItem的回調函數
changeItem: function(item){
this.setState({
curItem: item
});
},
render: function(){
return (
<div>
The curItem is: {this.state.curItem}
<List list={this.state.list} curItem={this.state.curItem} />
<SelectionButtons changeItem={this.changeItem}/>
</div>
)
}
});
var List = React.createClass({
render: function(){
var selectedStyle = {
color: 'white',
background: 'red'
};
return (
<ul>
{
(function(){
var self = this;
return this.props.list.map(function(item){
var itemStyle = (item == self.props.curItem) ? selectedStyle : {};
return (
<li style={itemStyle}>I am {item}!</li>
)
});
}.bind(this))()
}
</ul>
)
}
});
var SelectionButtons = React.createClass({
onClickItem: function(item){
this.props.changeItem(item);
},
render: function(){
return (
<div>
<button onClick={this.onClickItem.bind(this, 'item1')}>item1</button>
<button onClick={this.onClickItem.bind(this, 'item2')}>item2</button>
</div>
)
}
});
ReactDOM.render(
<MyContainer />,
document.getElementById('example')
);
如上述代碼所示,共享數據curItem作爲state放在父組件<MyContainer />中,將回調函數changeItem傳給<SelectionButtons />用於改變curItem,將curItem傳給<List />用於高亮當前被選擇的item。
2. 組件層次太深的噩夢
兄弟組件的溝通的解決方案就是找到兩個組件共同的父組件,一層一層的調用上一層的回調,再一層一層地傳遞props。如果組件樹嵌套太深,就會出現如下慘不忍睹的組件親戚調用圖。
下面就來說說如何避免這個組件親戚圖的兩個方法:全局事件和Context。
3. 全局事件
可以使用事件來實現組件間的溝通:改變數據的組件發起一個事件,使用數據的組件監聽這個事件,在事件處理函數中觸發setState來改變視圖或者做其他的操作。使用事件實現組件間溝通脫離了單向數據流機制,不用將數據或者回調函數一層一層地傳給子組件,可以避免出現上述的親戚圖。
事件模塊可以使用如EventEmitter或PostalJS這些第三方庫,也可以自己簡單實現一個:
var EventEmitter = {
_events: {},
dispatch: function (event, data) {
if (!this._events[event]) return; // no one is listening to this event
for (var i = 0; i < this._events[event].length; i++)
this._events[event][i](data);
},
subscribe: function (event, callback) {
if (!this._events[event]) this._events[event] = []; // new event
this._events[event].push(callback);
},
unSubscribe: function(event){
if(this._events && this._events[event]) {
delete this._events[event];
}
}
}
組件代碼如下:
var MyContainer = React.createClass({
render: function(){
return (
<div>
<CurItemPanel />
<SelectionButtons/>
</div>
)
}
});
var CurItemPanel = React.createClass({
getInitialState: function(){
return {
curItem: 'item1'
}
},
componentDidMount: function(){
var self = this;
EventEmitter.subscribe('changeItem', function(newItem){
self.setState({
curItem: newItem
});
})
},
componentWillUnmount: function(){
EventEmitter.unSubscribe('changeItem');
},
render: function(){
return (
<p>
The curItem is: {this.state.curItem}
</p>
)
}
});
var SelectionButtons = React.createClass({
onClickItem: function(item){
EventEmitter.dispatch('changeItem', item);
},
render: function(){
return (
<div>
<button onClick={this.onClickItem.bind(this, 'item1')}>item1</button>
<button onClick={this.onClickItem.bind(this, 'item2')}>item2</button>
</div>
)
}
});
ReactDOM.render(
<MyContainer />,
document.getElementById('example')
);
事件綁定和解綁可以分別放在componentDidMount和componentWillUnMount中。由於事件是全局的,最好保證在componentWillUnMount中解綁事件,否則,下一次初始化組件時事件可能會綁定多次。 使用事件模型,組件之間無論是父子關係還是非父子關係都可以直接溝通,從而解決了組件間層層回調傳遞的問題,但是頻繁地使用事件實現組件間溝通會使整個程序的數據流向越來越亂,因此,組件間的溝通還是要儘量遵循單向數據流機制。
4. context(上下文)
使用上下文可以讓子組件直接訪問祖先的數據或函數,無需從祖先組件一層層地傳遞數據到子組件中。
MyContainer組件:
var MyContainer = React.createClass({
getInitialState: function(){
return {
curItem: 'item1'
}
},
childContextTypes: {
curItem: React.PropTypes.any,
changeItem: React.PropTypes.any
},
getChildContext: function(){
return {
curItem: this.state.curItem,
changeItem: this.changeItem
}
},
changeItem: function(item){
this.setState({
curItem: item
});
},
render: function(){
return (
<div>
<CurItemWrapper />
<ListWrapper changeItem={this.changeItem}/>
</div>
)
}
});
childContextTypes用於驗證上下文的數據類型,這個屬性是必須要有的,否則會報錯。getChildContext用於指定子組件可直接訪問的上下文數據。
CurItemWrapper組件和CurItemPanel組件:
var CurItemWrapper = React.createClass({
render: function(){
return (
<div>
<CurItemPanel />
</div>
)
}
});
var CurItemPanel = React.createClass({
contextTypes: {
curItem: React.PropTypes.any
},
render: function(){
return (
<p>
The curItem is: {this.context.curItem}
</p>
)
}
});
在<CurItemPanel />通過this.context.curItem屬性訪問curItem,無需讓<CurItemWrapper />將curItem傳遞過來。必須在contextTypes中設置curItem的驗證類型,否則this.context是訪問不了curItem的。
ListWrapper組件和List組件:
var ListWrapper = React.createClass({
render: function(){
return (
<div>
<List />
</div>
)
}
});
var List = React.createClass({
contextTypes: {
changeItem: React.PropTypes.any
},
onClickItem: function(item){
this.context.changeItem(item);
},
render: function(){
return (
<ul>
<li onClick={this.onClickItem.bind(this, 'item1')}>I am item1, click me!</li>
<li onClick={this.onClickItem.bind(this, 'item2')}>I am item2, click me!</li>
</ul>
)
}
});
同上,<List />可以通過this.context.changeItem獲取<MyContainer />的改變curItem的changeItem函數。
5. Redux
爲了在React中更加清晰地管理數據,Facebook提出了Flux架構,而redux則是Flux的一種優化實現。
關於redux,另外一個比我帥氣的同事已經寫了一篇詳細的redux介紹博文,傳送門在下面,有興趣的可以去看看。
http://www.alloyteam.com/2015/09/react-redux/
當Redux與React搭配使用時,一般都是在最頂層組件中使用Redux。其餘內部組件僅僅是展示性的,發起dispatch的函數和其他數據都通過props傳入。然後,我們又會看到那熟悉的組件親戚調用圖:
如果使用全局事件解決方案,那麼redux中漂亮的,優雅的單向數據管理方式就會遭到破壞。於是,使用context就成了解決這種層層回調傳遞問題的首選方案,下面給出一個簡單例子:
index.js:
import { createStore, applyMiddleware } from 'redux';
import reducers from "./reducers"
import { Provider } from 'react-redux'
import React, {Component} from 'react';
import { render } from 'react-dom';
import App from './App';
let store = createStore(reducers);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
reducers.js:
export default function changeItem(state = {'curItem': 'item1'}, action){
switch(action.type) {
case 'CHANGE_ITEM':
return Object.assign({}, {
curItem: action.curItem
});
default:
return state;
}
}
actions.js:
export function changeItem(item) {
return {
type: 'CHANGE_ITEM',
curItem: item
}
}
App.js(組件代碼):
import React, {Component} from 'react';
import { connect, Provider } from 'react-redux';
import { changeItem } from './actions';
class App extends Component{
constructor(props, context) {
super(props, context);
}
getChildContext() {
return {
curItem: this.props.curItem,
changeItem: this.props.changeItem
}
}
render() {
return (
<div>
<CurItemPanel />
<List />
</div>
)
}
}
App.childContextTypes = {
curItem: React.PropTypes.any,
changeItem: React.PropTypes.any
};
class CurItemPanel extends Component {
constructor(props, context) {
super(props, context);
}
render() {
return (
<div>The curItem is: {this.context.curItem}</div>
)
}
}
CurItemPanel.contextTypes = {
curItem: React.PropTypes.any
};
class List extends Component {
constructor(props, context) {
super(props, context);
}
onClickItem (item){
this.context.changeItem(item);
}
render() {
return (
<ul>
<li onClick={this.onClickItem.bind(this, 'item1')}>I am item1, click me!</li>
<li onClick={this.onClickItem.bind(this, 'item2')}>I am item2, click me!</li>
</ul>
)
}
}
List.contextTypes = {
changeItem: React.PropTypes.any
};
let select = state => { return state};
function mapDispatchToProps(dispatch) {
return {
changeItem: function(item) {
dispatch(changeItem(item));
}
};
}
export default(connect(select, mapDispatchToProps))(App);
上述代碼中,Store是直接與智能組件<App />交互的,所以Store將state數據curItem和dispatch函數changeItem作爲props傳給了<App />。在<App />中將curItem數據和changeItem函數作爲上下文,作爲子組件的笨拙組件就可以之間通過上下文訪問這些數據,無需通過props獲取。
注:
1.redux的官方文檔中是使用ES6語法的,所以這裏的React代碼也使用ES6做例子
2.運行上述代碼需要構建代碼,大家可以在redux的github中下載redux帶構建代碼的examples,然後將代碼替換了再構建運行。
6. transdux
偶爾之間發現一個叫transdux的東西。這是一個類redux的數據溝通框架,作者的初衷是爲了讓用戶寫出比redux更簡潔的代碼,同時還能獲得[fl|re]dux的好處。用戶端使用該框架的話,可以解決下面一些redux中不好看的代碼寫法:
1)redux中需要創一個全局的store給Provider。Transdux中省略這個store。
2)redux與react搭配使用時,redux需要通過connect方法將數據和dispatch方法傳給redux。Transdux沒有connect。
3)redux需要把action當props傳下去,跟傳callback一樣。Trandux不會出現這種傳遞。
使用transdux需要以下步驟
(1)安裝trandux
npm install transdux –save
(2)把component包到Transdux裏
import React, {Component} from 'react';
import Transdux from 'transdux';
import App from './TransduxApp.js';
import { render } from 'react-dom';
render(
<Transdux>
<App />
</Transdux>,
document.getElementById('root')
);
(3)定義component能幹什麼,component的狀態如何改變
import React, {Component} from 'react';
import {mixin} from 'transdux'
import ChangeButton from './ChangeButton';
// 定義action是怎麼變的
let actions = {
addHello(obj, state, props) {
// 返回state
return {
msg: obj.msg
}
}
};
class App extends Component{
constructor(props){
super(props);
this.state = {msg: 'init'};
}
render() {
// 應該傳入調用了store.dispatch回調函數給笨拙組件
return (
<div>
{this.state.msg}
<ChangeButton />
</div>
)
}
}
export default mixin(App, actions);
(4)使用dispatch
import React, {Component} from 'react';
import {mixin} from 'transdux'
import minApp from './TransduxApp';
class ChangeButton extends Component{
click() {
this.dispatch(minApp, 'addHello', {'msg': 'hello world'});
}
render() {
return (
<div>
<button onClick={this.click.bind(this)}>change content</button>
</div>
)
}
}
export default mixin(ChangeButton, {});
mixin方法擴爲<ChangeButton />擴展了一個dispatch方法。dispatch方法需要三個參數:接手消息的組件、改變組件的actions、傳遞的對象。<ChangeButton />的按鈕事件處理函數調用了該dispatch後,會改變<App />中的狀態。
使用了Clojure的Channel通信機制,實現了組件與組件之間的直接通信。這種通信的效果類似與events,每個組件可以維護着自己的state,然後用mixin包裝自己傳給其他組件改變狀態。
Transdux的傳送門在下面,有興趣的同學可以去看看:
https://blog.oyanglul.us/javascript/react-transdux-the-clojure-approach-of-flux.html
小結
簡單的的組件溝通可以用傳props和callback的方法實現,然而,隨着項目規模的擴大,組件就會嵌套得越來越深,這時候使用這個方法就有點不太適合。全局事件可以讓組件直接溝通,但頻繁使用事件會讓數據流動變得很亂。如果兄弟組件共同的父組件嵌套得太深,在這個父組件設置context從而直接傳遞數據和callback到這兩個兄弟組件中。使用redux可以讓你整個項目的數據流向十分清晰,但是很容易會出現組件嵌套太深的情況,events和context都可以解決這個問題。Transdux是一個類redux框架,使用這個框架可以寫出比redux簡潔的代碼,又可以得到redux的好處。