上一篇分析了Flux出現的背景和原理,最核心的思想就是「組件化+單向數據流」。javascript
可是,Flux在設計上並不是完美,具體來講主要存在如下2個不足:前端
因爲Flux採用多Store設計,各個Store之間可能存在數據依賴。以flux-chat爲例:在這個聊天軟件裏,可能會有多我的給你發消息,好比Dave給你發了3條,Brian給你發了2條,當你點開某我的給你發的消息後,界面須要刷新,顯示你目前還有幾我的的未讀消息沒有查看:java
爲了解決這個需求,建立了3個Store:react
當你點開某個消息組時,顯然你須要先更新ThreadStore和MessageStore,而後再更新UnreadThreadStore。因爲Store的註冊順序是不肯定的,爲了應付這種依賴,Flux提供了waitFor()機制,每一個Store在註冊以後都會生成一個令牌(dispatchToken),經過等待令牌的方式確保其餘Store被優先更新。數據庫
所以UnreadThreadStore的代碼會寫成下面這個樣子:編程
Dispatcher.waitFor([
ThreadStore.dispatchToken,
MessageStore.dispatchToken
]);
switch (action.type) {
case ActionTypes.CLICK_THREAD:
UnreadThreadStore.emitChange();
break;
...
}
複製代碼
雖然能夠工做,可是總以爲不是很優雅,在一個Store中須要顯示地包含其餘Store的調用。固然你會說,乾脆把這3個Store的代碼糅到一塊兒,搞成一個Store不就好了?可是這樣又會致使代碼結構不夠清晰,不利於多模塊分工協做。redux
爲了兼顧這兩個方面,Redux使用全局惟一Store,外部可使用多個reducer來修改Store的不一樣部分,最後會把全部reducer的修改再組合成一個新的Store狀態。bash
所謂純函數,是指輸出只和輸入相關,相同的輸入必定會獲得相同的輸出。用專業一點的術語來講,純函數沒有「反作用」。咱們先來看看Flux中是怎麼修改狀態的:網絡
Dispatcher.register(action => {
switch(action.type) {
case ActionTypes.CLICK_THREAD:
_currentID = action.threadID;
ThreadStore.emitChange();
break;
...
}
複製代碼
能夠看到,是直接修改變量值,而後顯式發送一個change事件來通知View。app
咱們再來看看Redux中是怎麼修改狀態的:
export default function threadReducer(state = {}, action) {
switch (action.type) {
case ActionTypes.CLICK_THREAD: {
return { ...state, _currentID: action.threadID };
...
}
複製代碼
細心的人可能已經看出來了,主要有3點區別:
那麼有人會說了,爲啥要這麼作,好像也沒看到啥好處嘛?固然是有好處的,這樣能夠支持「時間旅行調試(Time Travel Debugging)」。所謂時間旅行調試,指的是能夠支持狀態的無限undo / redo。因爲state對象是被總體替換的,若是想回到上一個狀態從新執行,那麼直接替換成上一步的state對象就能夠了。
首先咱們要搞清楚,Redux解決了哪些問題?主要是如下3點:
1.如何在應用程序的整個生命週期內維持全部數據?
Redux是一個「狀態容器」。寫過React或者ReactNative的同窗可能會有感覺,若是多個頁面須要共享數據時,須要把數據一層層地傳遞下去,很是繁瑣。若是能有一個全局統一的地方存儲數據,當數據發生變化時自動通知View刷新界面,是否是很美好呢?所以,咱們須要一個「狀態容器」。
2.如何修改這些數據?
Redux借鑑了分佈式計算中的map-reduce的思想,把Store中的數據分割(map)成多個小的對象,經過純函數修改這些對象,最後再把全部的修改合併(reduce)成一個大的對象。修改數據的純函數被稱爲reducer。
3.如何把數據變動傳播到整個應用程序?
經過訂閱(subscribe)。若是你的View須要跟隨數據的變化動態刷新,能夠調用subscribe()註冊回調函數。在這一點上,Redux是很是粗粒度的,每次只要有新的action被分發,你都會收到通知。顯然,你須要對通知進行過濾,這意味着你可能會寫不少重複代碼。不過,這也是出於通用性和靈活性考慮,實際上Redux不只能夠用於React,也能夠用在Vue.js或者Angular上。能夠搭配特定框架相關的適配層好比react-redux來規避這些重複代碼。
說了這麼多,咱們來看一下Redux的基本框架:
和前一篇的Flux框架圖對比一下能夠發現,Redux去除了dispatcher組件(由於只有一個Store),增長了recuder組件(用於更新Store的不一樣部分)。下面詳細介紹各個部分的做用。
首先咱們須要建立一個全局惟一的Store,Redux提供了輔助函數createStore():
import { createStore } from 'redux'
var store = createStore(() => {})
複製代碼
你可能注意到了,createStore()須要提供一個參數,這個參數就是reducer。
前面介紹過,reducer就是一個純函數,輸入參數是state和action,輸出新的state。通常的代碼模板以下:
var reducer = (state = {}, action) => {
switch (action.type) {
case 'MY_ACTION':
return {...state, message: action.message}
default:
return state
}
}
複製代碼
須要注意的是,default分支必定要返回state,不然會致使狀態丟失。
好了,如今咱們有了reducer,能夠做爲參數傳遞給4.1節中的createStore()函數了。
createStore()只能接受一個reducer參數,若是咱們有多個reducer怎麼辦?這時須要使用另外一個輔助函數combineReducers():
import { combineReducers } from 'redux'
var reducer = combineReducers({
first: firstReducer,
second: secondReducer
})
複製代碼
combineReducers()會把多個reducer組合成一個,當有action過來時會依次調用每一個子reducer,因此實際上你能夠組織成一個樹狀結構。
所謂action,其實就是一個普通的javascript對象,通常會包含一個type屬性用於標識類型,以及一個payload屬性用於傳遞參數(名字能夠隨便取):
var action = {
type: 'MY_ACTION',
payload: { message: 'hello' }
}
複製代碼
那麼如何發送action呢?store提供了一個dispatch()函數:
store.dispatch(action)
複製代碼
所謂action creator,其實就是一個用來構建action對象的函數:
var actionCreator = (message) => {
return {
type: 'MY_ACTION',
payload: { message: message }
}
}
複製代碼
因此4.3節發送action的代碼也能夠寫成這樣:
store.dispatch(actionCreator('hello'))
複製代碼
當你發送了一個action,reducer被調用並完成狀態修改,那麼前端視是怎麼感知到狀態變化的呢?咱們須要經過subscribe()進行訂閱:
store.subscribe(() => {
let state = store.getState()
... ...
})
複製代碼
store的getState()函數能夠得到當前狀態的一個副本,而後就能夠刷新界面了,以React爲例,能夠調用this.setState()或者this.forceUpdate()觸發從新渲染。
當視圖組件比較多時,每次都要寫這段訂閱代碼會比較繁瑣,後面會介紹經過react-redux來簡化這一過程。
第3章的那張圖其實還少畫了個東西,叫作middleware(中間件)。那麼這個middleware是幹什麼用的呢?
在Web應用中常常會有異步調用,好比請求網絡、查詢數據庫什麼的。咱們首先發送一個action啓動異步任務,並但願在異步任務完成之後再更新狀態,應該如何實現呢?在Flux中,咱們能夠在dispatcher裏完成:首先啓動異步任務,而後在回調函數中再發送一個新的action去更新Store。可是Redux中去除了dispatcher的概念,你能調用的只有store的dispatch()函數而已,那咱們該怎麼辦呢?答案就是middleware。
因此,Redux的完整流程應參見下面這張動圖:
咱們先來看一個簡單的middleware的例子:
var thunkMiddleware = ({ dispatch, getState }) => {
return (next) => {
return (action) => {
return typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}
}
}
複製代碼
能夠發現,其實middleware就是一個三層嵌套的函數:
因此,實際上middleware能夠理解在action進入reducer以前進行了一次攔截。在這個例子裏,若是action是一個函數,咱們就不會把action繼續傳遞下去,而是調用這個函數去執行異步任務。當異步任務執行完畢後,咱們能夠調用dispatch()函數發送一個新的action,用於調用reducer更新狀態。
那麼咱們如何註冊一箇中間件呢?Redux提供了一個工具函數applyMiddleware(),能夠直接做爲createStore()的一個參數傳遞進去:
const store = createStore(
reducer,
applyMiddleware(myMiddleware1, myMiddleware2)
)
複製代碼
預告一下,後面一篇要介紹的redux-saga,其實就是一個Redux中間件。
Redux的設計主要考慮的是通用性和靈活性,若是想更好的配合React的組件化編程習慣,你可能須要react-redux。
Redux使用全局惟一的Store,另外當你須要發送action的時候,必須經過store的dispatch()函數。這對於一個有不少頁面的React應用來講,意味着只有兩種選擇:
這顯然極其繁瑣,幸運的是,React提供了Context機制,說白了就是全部頁面都能訪問的一個上下文對象:
react-redux利用React的Context機制進行了封裝,提供了<Provider>組件和connect()函數來實現store對象的全局可訪問性。
這是一個React組件,使用時須要把它包裹在應用層根組件的外面,而後把全局store對象賦值給它的store屬性:
import { Provider } from 'react-redux'
import store from './mystore'
export default class Application extends React.Component {
render () {
return (
<Provider store={ store }>
<Home />
</Provider>
)
}
}
複製代碼
Provider組件只是把store對象放進了Context中,若是你須要訪問它,還須要一些額外的代碼,react-redux提供了一個connect()函數來幫你完成這些工做。
實際上,connect()就幫你作了兩件事:
實現層面上,connect()採用了React的HOC(高階組件)技術,動態建立新組件及其實例:
那麼這個connect()怎麼用呢?咱們經過3個應用場景依次介紹。
1.你只是但願能在組件中使用dispatch()直接派發action
這是最簡單的狀況,你只須要在導出組件的時候加上connect()就能夠了:
export default connect()(MyComponent)
複製代碼
當你須要派發action的時候,能夠直接調用this.props.dispatch()。
2.你不想直接使用dispatch(),但願可以自動派發action
實際上你會發現,若是action不少的話,你須要不停地調用dispatch()函數。爲了使咱們的實現更加「聲明式」,最好是把派發邏輯封裝起來。實際上Redux中有一個輔助函數bindActionCreators()來完成這項工做,它會爲每一個action creator生成同名的函數,自動調用dispatch()函數:
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const boundActionCreators = bindActionCreators({ increment, decrement }, dispatch);
// 返回值:
// {
// increment: (...args) => dispatch(increment(...args)),
// decrement: (...args) => dispatch(decrement(...args)),
// }
複製代碼
這樣你就能夠直接調用boundActionCreators.increment()派發action了。那麼如何跟connect()聯繫起來呢?這裏須要用到它的第2個參數(第1個參數後面再介紹)mapDispatchToProps,舉個例子:
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ increment, decrement }, dispatch);
}
export default connect(null, mapDispatchToProps)(MyComponent)
複製代碼
這樣,你就能夠在組件中直接調用this.props.increment()函數了。
你覺得這樣就結束了?還有更簡單的方法,連bindActionCreators()都不用寫!你能夠直接提供一個對象,包含全部的action creator就好了(這被稱爲「對象簡寫」方式):
const mapDispatchToProps = { increment, decrement }
export default connect(null, mapDispatchToProps)(MyComponent)
複製代碼
注意:若是你提供了mapDispatchToProps參數,那麼默認狀況下dispatch就不會再注入到props中了。若是你還想使用this.props.dispatch(),能夠在mapDispatchToProps的返回值對象中加上dispatch屬性。
3.你但願訪問store中的數據
這應該是使用最多的場景,組件訪問store中的數據並刷新界面。根據「無狀態組件」設計原則,咱們不該該直接訪問store,而須要經過一個「selector函數」把store中的數據映射的props中進行訪問,這個「selector函數」就是conntect()的第1個參數mapStateToProps。舉個例子:
const mapStateToProps = (state = {}, ownProps) => {
return {
xxx: state.xxx
}
}
export default connect(mapStateToProps)(MyComponent)
複製代碼
這樣你在組件中就能夠經過this.props.xxx進行訪問了。另外,它還會幫你自動訂閱store,任什麼時候候store狀態數據發生變化,mapStateToProps都會被調用並致使界面從新渲染。除了第一個參數state以外,還有一個可選參數ownProps,若是你的組件須要用自身的props數據到store中檢索數據,能夠經過這個參數獲取。
固然,你能夠同時提供mapStateToProps和mapDispatchToProps參數,這樣你就能夠得到兩方面的功能:
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
複製代碼
最後,以一張思惟導圖結束本篇文章,下一篇介紹redux-saga。