ReactJS組件間溝通的一些方法

1.組件間的關係 javascript

1.1 父子組件 html

ReactJS中數據的流動是單向的,父組件的數據能夠經過設置子組件的props傳遞數據給子組件。若是想讓子組件改變父組件的數據,能夠在父組件中傳一個callback(回調函數)給子組件,子組件內調用這個callback便可改變父組件的數據。 java

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數據。 react

1.2 兄弟組件 git

當兩個組件不是父子關係,但有相同的父組件時,將這兩個組件稱爲兄弟組件。兄弟組件不能直接相互傳送數據,此時能夠將數據掛載在父組件中,由兩個組件共享:若是組件須要數據渲染,則由父組件經過props傳遞給該組件;若是組件須要改變數據,則父組件傳遞一個改變數據的回調函數給該組件,並在對應事件中調用。 github


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

 

2. 組件層次太深的噩夢 npm

兄弟組件的溝通的解決方案就是找到兩個組件共同的父組件,一層一層的調用上一層的回調,再一層一層地傳遞props。若是組件樹嵌套太深,就會出現以下慘不忍睹的組件親戚調用圖。 redux

share-parent-components

 

下面就來講說如何避免這個組件親戚圖的兩個方法:全局事件和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傳入。而後,咱們又會看到那熟悉的組件親戚調用圖:

share-parent-components

 

若是使用全局事件解決方案,那麼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的好處。

 

參考文章:

1. http://ctheu.com/2015/02/12/how-to-communicate-between-react-components/

2. https://blog.oyanglul.us/javascript/react-transdux-the-clojure-approach-of-flux.html  看咱們3天hackday都幹了些什麼

3. http://stackoverflow.com/questions/21285923/reactjs-two-components-communicating

4. https://blog.jscrambler.com/react-js-communication-between-components-with-contexts/

相關文章
相關標籤/搜索