React 數據流管理架構之 Redux 介紹
繼 非死book 提出 Flux 架構來管理 React 數據流后,相關架構開始百花齊放,本文簡單分析 React 中管理數據流的方式,以及對 Redux 進行較為仔細的介紹。
React
" A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES "
在 React 中,UI 以組件的形式來搭建,組件之間可以嵌套組合。另,React 中組件間通信的數據流是單向的,頂層組件可以通過 props 屬性向下層組件傳遞數據,而下層組件不能向上層組件傳遞數據,兄弟組件之間同樣不能。這樣簡單的單向數據流支撐起了 React 中的數據可控性。
那么,更全面的組件間通信形式該怎么實現呢?
-
嵌套組件間,上層組件向下層組件傳遞回調函數,下層組件觸發回調來更新上層組件的數據。
-
以事件的形式,使用發布訂閱的方式來通知數據更新。
-
Flux —- Fackbook 提出的管理 React 數據流的架構。Flux 不像一個框架,更是一種組織代碼的推薦思想。就像 “引導數據流流向的導流管”。
-
其他的 “導流管”。ReFlux,Redux 等。
前兩種形式其實也足夠在小應用中跑起來。但當項目越來越大的時候,管理數據的事件或回調函數將越來越多,也將越來越不好管理了。 對于后兩種形式,個人經過對比后,可以看出 Redux 對 Flux 架構的一些簡化。如 Redux 限定一個應用中只能有單一的 store,這樣的限定能夠讓應用中數據結果集中化,提高可控性。當然,不僅如此。
Redux
Redux 主要分為三個部分 Action、Reducer、及 Store
Action
在 Redux 中,action 主要用來傳遞操作 State 的信息,以 Javascript Plain Object 的形式存在,如
{ type : 'ADD_FILM' , name : 'Mission: Impossible' } |
在上面的 Plain Object 中,type 屬性是必要的,除了 type 字段外,action 對象的結構完全取決于你,建議盡可能簡單。type 一般用來表達處理 state 數據的方式。如上面的 'ADD_FILM' 表達要增加一個電影。而 name 表達了增加這個電影的電影名為 'Mission: Impossible'。那么,當我們需要表達增加另一部電影時,就需要另外一個action,如
{ type : 'ADD_FILM' , name : 'Minions' } |
上面寫法沒有任何問題,但細想,當我們增加的電影越來越多的時候,那這種直接聲明的 Plain Object 將越來越多,不好組織。實際上,我們可以通過創建函數來生產 action,這類函數統稱為 Action Creator,如
function addFilm ( name ) { return { type : 'ADD_FILM' , name : name } ; } |
這樣,通過調用 addFilm(name) 就可以得到對應的 Action,非常直接。
Reducer
有了 Action 來傳達需要操作的信息,那么就需要有根據這個信息來做對應操作的方法,這就是 Reducer。 Reducer 一般為簡單的處理函數,通過傳入舊的 state 和指示操作的 action 來更新 state,如
function films ( state = initialState , action ) { switch ( action . type ) { case 'ADD_FILM' : // 更新 state 中的 films 字段 return [ { id : state . films . reduce ( ( maxId , film ) = > Math . max ( film . id , maxId ) , - 1 ) + 1 , name : action . name } , . . . state ] ; case 'DELETE_FILM' : return state . films . filter ( film = > film . id !== action . id ) ; case 'SHOW_ALL_FILM' : return Object . assign ( { } , state , { visibilityFilter : action . filter } ) ; default : return state ; } |
上面代碼展示了 Reducer 根據傳入的 action.type 來匹配 case 進行不同的 state 更新。
顯然,當項目中存在越來越多的 action.type 時,上面的 films 函數( Reducer )將變得越來越大,越來越多的 case 將導致代碼不夠清晰。所以在代碼組織上,通常會將 Reducer 拆分成一個個小的 reducer,每個 reducer 分別處理 state 中的一部分數據,最終將處理后的數據合并成為整個 state。
在上面的代碼中,我們可以把 'ADD_FILM' 和 'DELETE_FILM' 歸為操作 state.films 的類,而 'SHOW_ALL_FILM' 為過濾顯示類,所以可以把大的 film Reducer 拆分成 filmReducer 和 filterReducer,如
1 filmReducer
function filmReducer ( state = [ ] , action ) { switch ( action . type ) { case 'ADD_FILM' : // 更新 state 中的 films 字段 return [ { id : state . films . reduce ( ( maxId , film ) = > Math . max ( film . id , maxId ) , - 1 ) + 1 , name : action . name } , . . . state ] ; case 'DELETE_FILM' : return state . films . filter ( film = > film . id !== action . id ) ; default : return state ; } } |
2 filterReducer
function filterReducer ( state , action ) { switch ( action . type ) { case 'SHOW_ALL_FILM' : return Object . assign ( { } , state , { visibilityFilter : action . filter } ) ; default : return state ; } } |
最后,通過組合函數將上面兩個 reducers 組合起來,如
function rootReducer ( state = { } , action ) { return { films : filmReducer ( state . films , action ) , filter : filterReducer ( state . filter , action ) } ; } |
上面的 rootReducer 將不同部分的 state 傳給對應的 reducer 處理,最終合并所有 reducer 的返回值,組成整個state。
實際上,Redux 提供了 combineReducers() 方法來做 rootReducer 所做的事情。使用 combineReducers 來重構 rootReducer,如
var rootReducer = combineReducers ( { films : filmReducer , filter : filterReducer } ) ; |
combineReducers() 將調用一系列 reducer,并根據對應的 key 來篩選出 state 中的一部分數據給相應的 reducer,這樣也意味著每一個小的 reducer 將只能處理 state 的一部分數據,如:filterReducer 將只能處理及返回 state.filter 的數據,如果需要使用到其他 state 數據,那還是需要為這類 reducer 傳入整個 state。
在 Redux 中,一個 action 可以觸發多個 reducer,一個 reducer 中也可以包含多種 action.type 的處理。屬于多對多的關系。
Store
回顧 Action 及 Reducer:
Action 用來表達操作消息,Reducer 根據 Action 來更新 State。
在 Redux 項目中,Store 是單一的。維護著一個全局的 State,并且根據 Action 來進行事件分發處理 State。可以看出 Store 是一個把 Action 和 Reducer 結合起來的對象。
Redux 提供了 createStore() 方法來 生產 Store,并提供三個 API,如
var store = createStore ( rootReducer ) ; // 其中 rootReducer 為頂級的 Reducer |
store 對象可以簡單的理解為如下形式
function createStore ( reducer , initialState ) { //閉包私有變量 var currentReducer = reducer ; var currentState = initialState ; var listeners = [ ] ; function getState ( ) { return currentState ; } function subscribe ( listener ) { listeners . push ( listener ) ; return function unsubscribe ( ) { var index = listeners . indexOf ( listener ) ; listeners . splice ( index , 1 ) ; } ; } function dispatch ( action ) { currentState = currentReducer ( currentState , action ) ; listeners . slice ( ) . forEach ( listener = > listener ( ) ) ; return action ; } //返回一個包含可訪問閉包變量的公有方法 return { dispatch , subscribe , getState } ; } |
store.getState() 用來獲取 state 數據。
store.subscribe(listener) 用于注冊監聽函數。每當 state 數據更新時,將會觸發監聽函數。
而 store.dispatch(action) 是用于將一個 action 對象發送給 reducer 進行處理。如
store . dispatch ( { type : 'ADD_FILM' , name : 'Mission: Impossible' } ) ; |
store 對象使得我們可以通過 store.dispatch(action) 來減少對 reducer 的直接調用,并且能夠更好地對 state 進行統一管理。沒有 store,可能會出現 reducer(currentState, action) 這樣的頻繁地傳入 state 參數的更新形式。
bindActionCreators
從上面的 Action 相關介紹中可知,我們使用了 ActionCreator 來生產 action。所以在實際的 store.dispatch(action) 中,我們需要這樣調用 store.dispatch(actionCreator(…args))。
借鑒 Store 對 reducer 的封裝(減少傳入 state 參數)。可以對 store.dispatch 進行再一層封裝,將多參數轉化為單參數的形式。 Redux 提供的 bindActionCreators 就做了這件事。如
var actionCreators = bindActionCreators ( actionCreators , store . dispatch ) ; |
現在,經 bindActionCreators 包裝過后的 action Creator 形成了具有改變全局 state 數據的多個函數,將這些函數分發到各個地方,即能通過調用這些函數來改變全局的 state。
Redux 中的函數傳遞及原理
當調用了具備操作全局 state 的函數時,將經過一系列的函數傳遞及調用,如
問:為什么不直接使用 reducer(currentState, {type:'ADD_FILM', name: 'Minions'})) 呢?
答:這樣做除了在代碼組織和擴展維護上提供了便利,同時也涵蓋了函數式編程的許多優點。
React-Redux
Redux 并不依賴于 React,它支持多種框架 Ember、Angular、jQuery 甚至純 JavaScript。但實際上,它更合適由 數據更新 UI 的框架。如 React、Deku。
上面的章節最終通過 bindActionCreators 得到具有操作全局 state 的函數集合,在與 React 搭配時,就會將這些函數分發到各個對應的組件中,從而組件具備了操作全局的 state 的功能。在上節中可以得到,調用操作全局 state 的函數,最終將更新 state。當 redux 與 react 結合,在更新 state 時,將會觸發 重新渲染 組件的函數,進而組件得到更新。
react-redux 主要提供兩個組件來實現上述功能。
Connect
Connect 組件主要為 React 組件提供 store 中的部分 state 數據 及 dispatch 方法,這樣 React 組件就可以通過 dispatch 來更新全局 state。在 React 組件中,如果你希望讓組件通過調用函數來更新 state,可以通過使用 const actions = bindActionCreators(FilmActions, dispatch); 將 actions 和 dispatch 揉在一起,成為具備操作 store.state 的 actions。最終將 actions 和 state(state.films)以 props 形式傳入子組件中。如
import { connect } from 'react-redux' ; import * as addFlim from '../actions/films' ; // 其他模塊引入.. class FilmApp extends Component { render ( ) { // 從 react-redux 注入 const { todos , dispatch } = this . props ; // 生成具有操作 state 能力的 actions const actions = bindActionCreators ( FilmActions , dispatch ) ; // 為各個 React 組件提供 state 數據 及 actions return ( < div > < Header films = { films } actions = { actions } / > < Section films = { films } deleteFilm = { actions . deleteFilm } / > < / div > ) ; } } // state 將由 store 提供 function select ( state ) { return { films : state . films } ; } // 最終暴露 經 connect 處理后的組件 export default connect ( select ) ( FilmApp ) ; |
由上,在 redux 提供的 connect 函數中,select 函數用于篩選 state 的部分數據,最終和 dispatch 以 props 的形式傳給 React 組件(FilmApp)。FilmApp 就可通過 this.props 來得到 store 中的 state 及 dispatch。
在 redux 中,沒有與 redux 有直接關聯的組件稱為木偶組件,如 FilmApp 下的子組件,不理外面紛紛擾擾,只知道自己擁有了 state 及 具備操作 state 數據的 actions 方法。
當木偶組件使用 actions 方法,更新了 store.state 的數據時,將會觸發 store 中的 subscribe 所注冊的函數。而其中一個注冊函數,就在 Connect 組件中靜默注冊了。
// 在 Connect 中 this . store . subscribe ( this . handleChange . bind ( this ) ) ; |
即當 actions 更改了 state 時,會調用注冊函數 handleChange。從而進行 “阿米諾骨牌式” 的函數執行連鎖反應。更新了 state,并使用新的數據重新 render 組件。實際上是為智能組件 FilmApp(傳入 connect 的組件)傳入新的 props,因為各個子元素是通過引用父級組件的 props,所以將進行一級一級的差異數據更新,最終效果就是頁面更新了。
實際上,這里與簡單的發布訂閱模式類似。使用 store.subscribe(cb); 來訂閱一個回調函數,子組件進行 action 操作 store.state 時進行發布,執行了回調函數。
在 react-redux 中,數據的流向及對應的反應,如
Provider
Connect 組件需要 store。這個需求由 Redux 提供的另一個組件 Provider 來提供。源碼中,Provider 繼承了 React.Component,所以可以以 React 組件的形式來為 Provider 注入 store,從而使得其子組件能夠在上下文中得到 store 對象。如
< Provider store = { store } > { ( ) = > < FilmApp / > } < / Provider > |
在 React 0.13 及以前的版本中,Provider 渲染子組件是通過執行 children(),如
Provider . prototype . render = function render ( ) { var children = this . props . children ; return children ( ) ; } ; |
所以在 React 0.13 及以前的版本中,Provider 的子組件必須是一個函數。這個問題在 React 0.14 中修復。
更多
編輯狀態的實時預覽 redux-dev-tools https://github.com/gaearon/redux-devtools
大量的相關參考 awesome-redux https://github.com/xgrommx/awesome-redux