剛入門React可能會因為React的單向資料流的特性而遇到元件間溝通的麻煩,這篇文章主要就說一說如何解決元件間溝通的問題。
1.元件間的關係
1.1 父子元件
ReactJS中資料的流動是單向的,父元件的資料可以通過設定子元件的props傳遞資料給子元件。如果想讓子元件改變父元件的資料,可以在父元件中傳一個callback(回撥函式)給子元件,子元件內呼叫這個callback即可改變父元件的資料。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
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') ); |
是的父元件,通過props傳遞list資料給元件,如果中的list改變,會重新渲染列表資料。而可以通過傳來的changeItem函式,改變的curItem資料。
1.2 兄弟元件
當兩個元件不是父子關係,但有相同的父元件時,將這兩個元件稱為兄弟元件。兄弟元件不能直接相互傳送資料,此時可以將資料掛載在父元件中,由兩個元件共享:如果元件需要資料渲染,則由父元件通過props傳遞給該元件;如果元件需要改變資料,則父元件傳遞一個改變資料的回撥函式給該元件,並在對應事件中呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
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放在父元件中,將回撥函式changeItem傳給用於改變curItem,將curItem傳給用於高亮當前被選擇的item。
2. 元件層次太深的噩夢
兄弟元件的溝通的解決方案就是找到兩個元件共同的父元件,一層一層的呼叫上一層的回撥,再一層一層地傳遞props。如果元件樹巢狀太深,就會出現如下慘不忍睹的元件親戚呼叫圖。
下面就來說說如何避免這個元件親戚圖的兩個方法:全域性事件和Context。
3. 全域性事件
可以使用事件來實現元件間的溝通:改變資料的元件發起一個事件,使用資料的元件監聽這個事件,在事件處理函式中觸發setState來改變檢視或者做其他的操作。使用事件實現元件間溝通脫離了單向資料流機制,不用將資料或者回撥函式一層一層地傳給子元件,可以避免出現上述的親戚圖。
事件模組可以使用如EventEmitter或PostalJS這些第三方庫,也可以自己簡單實現一個:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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]; } } } |
元件程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
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元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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> ) } }); |
在通過this.context.curItem屬性訪問curItem,無需讓將curItem傳遞過來。必須在contextTypes中設定curItem的驗證型別,否則this.context是訪問不了curItem的。
ListWrapper元件和List元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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> ) } }); |
同上,可以通過this.context.changeItem獲取的改變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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 6 |
export function changeItem(item) { return { type: 'CHANGE_ITEM', curItem: item } } |
App.js(元件程式碼):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
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是直接與智慧元件互動的,所以Store將state資料curItem和dispatch函式changeItem作為props傳給了。在中將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裡
1 2 3 4 5 6 7 8 9 10 11 |
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的狀態如何改變
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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方法擴為擴充套件了一個dispatch方法。dispatch方法需要三個引數:接手訊息的元件、改變元件的actions、傳遞的物件。的按鈕事件處理函式呼叫了該dispatch後,會改變中的狀態。
使用了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/