React 數據流管理架構之 Redux 介紹

jopen 9年前發布 | 120K 次閱讀 Redux 移動開發
 

繼 非死book 提出 Flux 架構來管理 React 數據流后,相關架構開始百花齊放,本文簡單分析 React 中管理數據流的方式,以及對 Redux 進行較為仔細的介紹。

React

" A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES "

在 React 中,UI 以組件的形式來搭建,組件之間可以嵌套組合。另,React 中組件間通信的數據流是單向的,頂層組件可以通過 props 屬性向下層組件傳遞數據,而下層組件不能向上層組件傳遞數據,兄弟組件之間同樣不能。這樣簡單的單向數據流支撐起了 React 中的數據可控性。

那么,更全面的組件間通信形式該怎么實現呢?

  1. 嵌套組件間,上層組件向下層組件傳遞回調函數,下層組件觸發回調來更新上層組件的數據。

  2. 以事件的形式,使用發布訂閱的方式來通知數據更新。

  3. Flux —- Fackbook 提出的管理 React 數據流的架構。Flux 不像一個框架,更是一種組織代碼的推薦思想。就像 “引導數據流流向的導流管”。

  4. 其他的 “導流管”。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 的函數時,將經過一系列的函數傳遞及調用,如

React 數據流管理架構之 Redux 介紹

問:為什么不直接使用 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 中,數據的流向及對應的反應,如

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!