今天來看一下react組件之間是怎么進行通訊的。
react推崇的是單向數據流,自上而下進行數據的傳遞,但是由下而上或者不在一條數據流上的組件之間的通信就會變的復雜。解決通信問題的方法很多,如果只是父子級關系,父級可以將一個回調函數當作屬性傳遞給子級,子級可以直接調用函數從而和父級通信。
組件層級嵌套到比較深,可以使用上下文getChildContext來傳遞信息,這樣在不需要將函數一層層往下傳,任何一層的子級都可以通過this.context直接訪問。
兄弟關系的組件之間無法直接通信,它們只能利用同一層的上級作為中轉站。而如果兄弟組件都是最高層的組件,為了能夠讓它們進行通信,必須在它們外層再套一層組件,這個外層的組件起著保存數據,傳遞信息的作用,這其實就是redux所做的事情。
組件之間的信息還可以通過全局事件來傳遞。不同頁面可以通過參數傳遞數據,下個頁面可以用location.param來獲取。其實react本身很簡單,難的在于如何優雅高效的實現組件之間數據的交流。
Redux
首先,redux并不是必須的,它的作用相當于在頂層組件之上又加了一個組件,作用是進行邏輯運算、儲存數據和實現組件尤其是頂層組件的通信。如果組件之間的交流不多,邏輯不復雜,只是單純的進行視圖的渲染,這時候用回調,context就行,沒必要用redux,用了反而影響開發速度。但是如果組件交流特別頻繁,邏輯很復雜,那redux的優勢就特別明顯了。我第一次做react項目的時候并沒有用redux,所有的邏輯都是在組件內部實現,當時為了實現一個邏輯比較復雜的購物車,洋洋灑灑居然寫了800多行代碼,回頭一看我自己都不知道寫的是啥,畫面太感人。
先簡單說一下redux和react是怎么配合的。react-redux提供了connect和Provider兩個好基友,它們一個將組件與redux關聯起來,一個將store傳給組件。組件通過dispatch發出action,store根據action的type屬性調用對應的reducer并傳入state和這個action,reducer對state進行處理并返回一個新的state放入store,connect監聽到store發生變化,調用setState更新組件,此時組件的props也就跟著變化。
流程是這個樣子的:
值得注意的是connect,Provider,mapStateToProps,mapDispatchToProps是react-redux提供的,redux本身和react沒有半毛錢關系,它只是數據處理中心,沒有和react產生任何耦合,是react-redux讓它們聯系在一起。
接下來具體分析一下,redux以及react-redux到底是怎么實現的。
明顯比第一張要復雜,其實兩張圖說的是同一件事。從上而下慢慢分析:
先說說redux:
redux主要由三部分組成:store,reducer,action。
store是一個對象,它有四個主要的方法:
- 1、dispatch:
用于action的分發——在createStore中可以用middleware中間件對dispatch進行改造,比如當action傳入dispatch會立即觸發reducer,有些時候我們不希望它立即觸發,而是等待異步操作完成之后再觸發,這時候用redux-thunk對dispatch進行改造,以前只能傳入一個對象,改造完成后可以傳入一個函數,在這個函數里我們手動dispatch一個action對象,這個過程是可控的,就實現了異步。
- 2、subscribe:
監聽state的變化——這個函數在store調用dispatch時會注冊一個listener監聽state變化,當我們需要知道state是否變化時可以調用,它返回一個函數,調用這個返回的函數可以注銷監聽。 let unsubscribe = store.subscribe(() => {console.log('state發生了變化')})
- 3、getState:
獲取store中的state——當我們用action觸發reducer改變了state時,需要再拿到新的state里的數據,畢竟數據才是我們想要的。getState主要在兩個地方需要用到,一是在dispatch拿到action后store需要用它來獲取state里的數據,并把這個數據傳給reducer,這個過程是自動執行的,二是在我們利用subscribe監聽到state發生變化后調用它來獲取新的state數據,如果做到這一步,說明我們已經成功了。
- 4、replaceReducer:
替換reducer,改變state修改的邏輯。
store可以通過createStore()方法創建,接受三個參數,經過combineReducers合并的reducer和state的初始狀態以及改變dispatch的中間件,后兩個參數并不是必須的。store的主要作用是將action和reducer聯系起來并改變state。
- action:
action是一個對象,其中type屬性是必須的,同時可以傳入一些數據。action可以用actionCreactor進行創造。dispatch就是把action對象發送出去。
- reducer:
reducer是一個函數,它接受一個state和一個action,根據action的type返回一個新的state。根據業務邏輯可以分為很多個reducer,然后通過combineReducers將它們合并,state樹中有很多對象,每個state對象對應一個reducer,state對象的名字可以在合并時定義。
像這個樣子:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
- combineReducers:
其實它也是一個reducer,它接受整個state和一個action,然后將整個state拆分發送給對應的reducer進行處理,所有的reducer會收到相同的action,不過它們會根據action的type進行判斷,有這個type就進行處理然后返回新的state,沒有就返回默認值,然后這些分散的state又會整合在一起返回一個新的state樹。
接下來分析一下整體的流程,首先調用store.dispatch將action作為參數傳入,同時用getState獲取當前的狀態樹state并注冊subscribe的listener監聽state變化,再調用combineReducers并將獲取的state和action傳入。combineReducers會將傳入的state和action傳給所有reducer,reducer會根據state的key值獲取與自己對應的state,并根據action的type返回新的state,觸發state樹的更新,我們調用subscribe監聽到state發生變化后用getState獲取新的state數據。
redux的state和react的state兩者完全沒有關系,除了名字一樣。
上面分析了redux的主要功能,那么react-redux到底做了什么?
React-Redux
如果只使用redux,那么流程是這樣的:
component --> dispatch(action) --> reducer --> subscribe --> getState --> component
用了react-redux之后流程是這樣的:
component --> actionCreator(data) --> reducer --> component
store的三大功能:dispatch,subscribe,getState都不需要手動來寫了。react-redux幫我們做了這些,同時它提供了兩個好基友Provider和connect。
Provider是一個組件,它接受store作為props,然后通過context往下傳,這樣react中任何組件都可以通過contex獲取store。也就意味著我們可以在任何一個組件里利用dispatch(action)來觸發reducer改變state,并用subscribe監聽state的變化,然后用getState獲取變化后的值。但是并不推薦這樣做,它會讓數據流變的混亂,過度的耦合也會影響組件的復用,維護起來也更麻煩。
connect --connect(mapStateToProps, mapDispatchToProps, mergeProps, options)是一個函數,它接受四個參數并且再返回一個函數--wrapWithConnect,wrapWithConnect接受一個組件作為參數wrapWithConnect(component),它內部定義一個新組件Connect(容器組件)并將傳入的組件(ui組件)作為Connect的子組件然后return出去。
所以它的完整寫法是這樣的:connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)
mapStateToProps(state, [ownProps]):
mapStateToProps 接受兩個參數,store的state和自定義的props,并返回一個新的對象,這個對象會作為props的一部分傳入ui組件。我們可以根據組件所需要的數據自定義返回一個對象。
ownProps的變化也會觸發mapStateToProps
function mapStateToProps(state) {
return { todos: state.todos };
}
** mapDispatchToProps(dispatch, [ownProps]):**
mapDispatchToProps如果是對象,那么會和store綁定作為props的一部分傳入ui組件。如果是個函數,它接受兩個參數,bindActionCreators會將action和dispatch綁定并返回一個對象,這個對象會和ownProps一起作為props的一部分傳入ui組件。所以不論mapDispatchToProps是對象還是函數,它最終都會返回一個對象,如果是函數,這個對象的key值是可以自定義的
function mapDispatchToProps(dispatch) {
return {
todoActions: bindActionCreators(todoActionCreators, dispatch),
counterActions: bindActionCreators(counterActionCreators, dispatch)
};
}
mapDispatchToProps返回的對象其屬性其實就是一個個actionCreator,因為已經和dispatch綁定,所以當調用actionCreator時會立即發送action,而不用手動dispatch。ownProps的變化也會觸發mapDispatchToProps。
mergeProps(stateProps, dispatchProps, ownProps):
將mapStateToProps() 與 mapDispatchToProps()返回的對象和組件自身的props合并成新的props并傳入組件。默認返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。
options:
pure = true 表示Connect容器組件將在shouldComponentUpdate中對store的state和ownProps進行淺對比,判斷是否發生變化,優化性能。為false則不對比。
其實connect函數并沒有做什么,大部分的邏輯都是在它返回的wrapWithConnect函數內實現的,確切的說是在wrapWithConnect內定義的Connect組件里實現的。
下面是一個完整的 react --> redux --> react 流程:
一、Provider組件接受redux的store作為props,然后通過context往下傳。
二、connect函數在初始化的時候會將mapDispatchToProps對象綁定到store,如果mapDispatchToProps是函數則在Connect組件獲得store后,根據傳入的store.dispatch和action通過bindActionCreators進行綁定,再將返回的對象綁定到store,connect函數會返回一個wrapWithConnect函數,同時wrapWithConnect會被調用且傳入一個ui組件,wrapWithConnect內部使用class Connect extends Component定義了一個Connect組件,傳入的ui組件就是Connect的子組件,然后Connect組件會通過context獲得store,并通過store.getState獲得完整的state對象,將state傳入mapStateToProps返回stateProps對象、mapDispatchToProps對象或mapDispatchToProps函數會返回一個dispatchProps對象,stateProps、dispatchProps以及Connect組件的props三者通過Object.assign(),或者mergeProps合并為props傳入ui組件。然后在ComponentDidMount中調用store.subscribe,注冊了一個回調函數handleChange監聽state的變化。
三、此時ui組件就可以在props中找到actionCreator,當我們調用actionCreator時會自動調用dispatch,在dispatch中會調用getState獲取整個state,同時注冊一個listener監聽state的變化,store將獲得的state和action傳給combineReducers,combineReducers會將state依據state的key值分別傳給子reducer,并將action傳給全部子reducer,reducer會被依次執行進行action.type的判斷,如果有則返回一個新的state,如果沒有則返回默認。combineReducers再次將子reducer返回的單個state進行合并成一個新的完整的state。此時state發生了變化。dispatch在state返回新的值之后會調用所有注冊的listener函數其中包括handleChange函數,handleChange函數內部首先調用getState獲取新的state值并對新舊兩個state進行淺對比,如果相同直接return,如果不同則調用mapStateToProps獲取stateProps并將新舊兩個stateProps進行淺對比,如果相同,直接return結束,不進行后續操作。如果不相同則調用this.setState()觸發Connect組件的更新,傳入ui組件,觸發ui組件的更新,此時ui組件獲得新的props,react --> redux --> react 的一次流程結束。
上面的有點復雜,簡化版的流程是:
一、Provider組件接受redux的store作為props,然后通過context往下傳。
二、connect函數收到Provider傳出的store,然后接受三個參數mapStateToProps,mapDispatchToProps和組件,并將state和actionCreator以props傳入組件,這時組件就可以調用actionCreator函數來觸發reducer函數返回新的state,connect監聽到state變化調用setState更新組件并將新的state傳入組件。
connect可以寫的非常簡潔,mapStateToProps,mapDispatchToProps只不過是傳入的回調函數,connect函數在必要的時候會調用它們,名字不是固定的,甚至可以不寫名字。
簡化版本:
connect(state => state, action)(Component);