Redux使用中的幾個點:javascript
在傳統的 MVC 架構中,咱們能夠根據須要建立無數個 Model,而 Model 之間能夠互相監聽、觸發事件甚至循環或嵌套觸發事件,這些在 Redux 中都是不容許的。由於在 Redux 的思想裏,一個應用永遠只有惟一的數據源。
實際上,使用單一數據源的好處在於整個應用狀態都保存在一個對象中,這樣咱們隨時能夠提取出整個應用的狀態進行持久化(好比實現一個針對整個應用的即時保存功能)。此外,這樣的設計也爲服務端渲染提供了可能。java
在 Redux 中,咱們並不會本身用代碼來定義一個 store。取而代之的是,咱們定義一個 reducer,它的功能是根據當前觸發的 action 對當前應用的狀態(state)進行迭代,這裏咱們並無直接修改應用的狀態,而是返回了一份全新的狀態。react
Redux 提供的 createStore 方法會根據 reducer 生成 store。最後,咱們能夠利用 store. dispatch
方法來達到修改狀態的目的。編程
在 Redux 裏,咱們經過定義 reducer 來肯定狀態的修改,而每個 reducer 都是純函數,這意味着它沒有反作用,即接受必定的輸入,一定會獲得必定的輸出。redux
這樣設計的好處不只在於 reducer 裏對狀態的修改變得簡單、純粹、可測試,更有意思的是,Redux 利用每次新返回的狀態生成酷炫的時間旅行(time travel)調試方式,讓跟蹤每一次由於觸發 action 而改變狀態的結果成爲了可能。api
咱們從store的誕生開始提及。create store函數API文檔以下:數組
createStore(reducer, [initialState], enhancer)
能夠看出,它接受三個參數:reducer、initialState 和 enhancer 。Store enhancer 是一個組合 store creator 的高階函數,返回一個新的強化過的 store creator。這與 middleware 類似,它也容許你經過複合函數改變 store 接口。緩存
再來看看他的返回值:閉包
{ dispatch: f (action), getState: f (), replaceReducer: f (nextReducer), subscribe: f (listener), Symbol(observable): f () }
store的返回值就是一個普通對象,裏面有幾個經常使用的方法:架構
這裏挑幾個方法介紹:
在完成基本的參數校驗以後,在 createStore 中聲明以下變量及 getState 方法:
var currentReducer = reducer var currentState = initialState var listeners = [] // 當前監聽 store 變化的監聽器 var isDispatching = false // 某個 action 是否處於分發的處理過程當中 /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */ function getState() { return currentState }
getState方法就是簡單返回當前state,若是state沒有被reducer處理過,他就是initialState。
在 getState 以後,定義了 store 的另外一個方法 subscribe:
function subscribe(listener) { listeners.push(listener) var isSubscribed = true return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false var index = listeners.indexOf(listener) listeners.splice(index, 1) } }
Store 容許使用store.subscribe
方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。
顯然,只要把 View 的更新函數(對於 React 項目,就是組件的render
方法或setState
方法)放入listen
,就會實現 View 的自動渲染。你可能會感到奇怪,好像咱們在 Redux 應用中並無使用 store.subscribe 方法?事實上,
React Redux 中的 connect 方法隱式地幫咱們完成了這個工做。
store.subscribe
方法返回一個函數,調用這個函數就能夠解除監聽。
dispatch是redux的核心方法:
function dispatch(action) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } listeners.slice().forEach(listener => listener()) return action }
判斷當前是否處於某個 action 的分發過程當中,這個檢查主要是爲了不在 reducer 中分發 action 的狀況,由於這樣作可能致使分發死循環,同時也增長了數據流動的複雜度。
確認當前不屬於分發過程當中後,先設定標誌位,而後將當前的狀態和 action 傳給當前的reducer,用於生成最新的 state。這看起來一點都不復雜,這也是咱們反覆強調的 reducer 工做過程——純函數、接受狀態和 action 做爲參數,返回一個新的狀態。
在獲得新的狀態後,依次調用全部的監聽器,通知狀態的變動。須要注意的是,咱們在通知監聽器變動發生時,並無將最新的狀態做爲參數傳遞給這些監聽器。這是由於在監聽器中,咱們能夠直接調用 store.getState() 方法拿到最新的狀態。
最終,處理以後的 action 會被 dispatch 方法返回。
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.'); } currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); }
這是爲了拿到全部 reducer 中的初始狀態(你是否還記得在定義 reducer 時,第一個參數爲previousState,若是該參數爲空,咱們提供默認的 initialState)。只有全部的初始狀態都成功獲取後,Redux 應用纔能有條不紊地開始運做。
It provides a third-party extension point between dispatching an action, and the moment it reaches
the reducer
它提供了一個分類處理 action 的機會。在middleware 中,你能夠檢閱每個流過的 action,挑選出特定類型的action 進行相應操做,給你一次改變 action 的機會。
常規的同步數據流模式的流程圖以下:
不一樣業務需求下,好比執行action以前和以後都要打log;action觸發一個異步的請求,請求回來以後渲染view等。須要爲這一類的action添加公共的方法或者處理,使用redux middleware流程圖以下:
每個 middleware 處理一個相對獨立的業務需求,經過串聯不一樣的 middleware 實現變化多樣的功能。好比上面的業務,咱們把處理log的代碼封裝成一個middleware,處理異步的也是一個middleware,二者串聯,卻又相互獨立。
使用middleware以後,action觸發的dispatch並非原來的dispatch,而是通過封裝的new dispatch,在這個new dispatch中,按照順序依次執行每一個middleware,最後調用原生的dispatch。
咱們來看下logger middleware如何實現的:
export default store => next => action => { console.log('dispatch:', action); next(action); console.log('finish:', action); }
這裏代碼十分簡潔,就是在next調用下一個middleware以前和以後,分別打印兩次。
Redux 提供了 applyMiddleware 方法來加載 middleware,該方法的源碼以下:
import compose from './compose'; export default function applyMiddleware(...middlewares) { return function (next) { return function (reducer, initialState) { let store = next(reducer, initialState); let dispatch = store.dispatch; let chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action), }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch, }; } } }
其中compose源碼以下:
function compose(...funcs) { return arg => funcs.reduceRight((composed, f) => f(composed), arg); }
使用的時候,以下:
const newStore = applyMiddleware([mid1, mid2, mid3, ...])(createStore)(reducer, initialState);
ok,相關源碼已就位,咱們來詳細解析一波。
函數式編程思想設計 :middleware 的設計有點特殊,是一個層層包裹的匿名函數,這實際上是函數式編程中的
currying,它是一種使用匿名單參數函數來實現多參數函數的方法。applyMiddleware 會對 logger 這個middleware 進行層層調用,動態地將 store 和 next 參數賦值。currying 的 middleware 結構的好處主要有如下兩點。
給 middleware 分發 store:newStore建立完成以後,applyMiddleware 方法陸續得到了3個參數,第一個是 middlewares 數組[mid1, mid2, mid3, ...],第二個是 Redux 原生的 createStore ,最後一個是 reducer。而後,咱們能夠看到 applyMiddleware 利用 createStore 和 reducer 建立了一個 store。而 store 的 getState方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量 store:
const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action), }; chain = middlewares.map(middleware => middleware(middlewareAPI));
而後,讓每一個 middleware 帶着 middlewareAPI 這個參數分別執行一遍。執行完後,得到 chain數組 [f1, f2, ... , fx, ..., fn],它保存的對象是第二個箭頭函數返回的匿名函數。由於是閉包,每一個匿名函數均可以訪問相同的 store,即 middlewareAPI。
middlewareAPI 中的 dispatch 爲何要用匿名函數包裹呢?咱們用 applyMiddleware 是爲了改造 dispatch,因此 applyMiddleware 執行完後,dispatch 是變化了的,而 middlewareAPI 是 applyMiddleware 執行中分發到各個 middleware 的,因此必須用匿名函數包裹 dispatch,這樣只要 dispatch 更新了,middlewareAPI 中的 dispatch 應用也會發生變化。
組合串聯 middleware:這一層只有一行代碼,倒是 applyMiddleware 精華之所在dispatch = compose(...chain)(store.dispatch);
,其中 compose 是函數式編程中的組合,它將 chain 中的全部匿名函數 [f1, f2, ... , fx, ..., fn]組裝成一個新的函數,即新的 dispatch。當新 dispatch 執行時,[f1, f2, ... , fx, ..., fn],從右到左依次執行。
compose(...funcs) 返回的是一個匿名函數,其中 funcs 就是 chain 數組。當調用 reduceRight時,依次從 funcs 數組的右端取一個函數 fx 拿來執行,fx 的參數 composed 就是前一次 fx+1 執行的結果,而第一次執行的 fn(n 表明 chain 的長度)的參數 arg 就是 store.dispatch。因此,當 compose 執行完後,咱們獲得的 dispatch 是這樣的,假設 n = 3:
dispatch = f1(f2(f3(store.dispatch))));
這時調用新 dispatch,每個 middleware 就依次執行了。
在 middleware 中調用 dispatch 會發生什麼:通過 compose 後,全部的 middleware 算是串聯起來了。但是還有一個問題,在分發 store 時,咱們提到過每一個 middleware 均可以訪問 store,即 middlewareAPI 這個變量,也能夠拿到 store 的dispatch 屬性。那麼,在 middleware 中調用 store.dispatch() 會發生什麼,和調用 next() 有區別嗎?如今咱們來講明二者的不一樣:
const logger = store => next => action => { console.log('dispatch:', action); next(action); console.log('finish:', action); }; const logger = store => next => action => { console.log('dispatch:', action); store.dispatch(action); console.log('finish:', action); };
在分發 store 時咱們解釋過,middleware 中 store 的 dispatch 經過匿名函數的方式和最終compose 結束後的新 dispatch 保持一致,因此,在 middleware 中調用 store.dispatch() 和在其餘任何地方調用的效果同樣。而在 middleware 中調用 next(),效果是進入下一個 middleware,下圖就是redux middleware最著名的洋蔥模型圖。
若是一個項目過大,咱們一般按模塊來寫reducer,可是redux create store只接受一個reducer參數,因此咱們須要合併reducer。這裏就用到了redux提供的combineReducer
輔助函數:
combineReducers({ layout, home, ...asyncReducers })
這個函數用起來很簡單,就是傳入一個對象,key是模塊reducer對應的名字, 值是對應reducer。值是一個function,至關因而一個新的reducer,源碼以下:
export default function combineReducers(reducers) { var reducerKeys = Object.keys(reducers) var finalReducers = {} for (var i = 0; i < reducerKeys.length; i++) { var key = reducerKeys[i] if (process.env.NODE_ENV !== 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } var finalReducerKeys = Object.keys(finalReducers) if (process.env.NODE_ENV !== 'production') { var unexpectedKeyCache = {} } var sanityError try { assertReducerSanity(finalReducers) } catch (e) { sanityError = e } return function combination(state = {}, action) { if (sanityError) { throw sanityError } if (process.env.NODE_ENV !== 'production') { var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) if (warningMessage) { warning(warningMessage) } } var hasChanged = false var nextState = {} for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i] var reducer = finalReducers[key] var previousStateForKey = state[key] var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }
源碼不是不少,除去一些驗證代碼,剩下的就是說:return一個function,咱們暫時稱呼他combination,就至關因而與一個總的reducer,每次action都會走到combination中,combination會遍歷輸入的reducer,將action放到每一個reducer中執行一下,計算出返回結果就是nextState,nextState於previousState若是!==說明改變了,返回nextState,不然返回執行以前的state。
這也解釋了不一樣模塊actionType若是相同的話,兩個模塊的reducer都會走一遍的問題,在actionType名稱前面加上模塊前綴便可解決問題。
Provider與Connet組件都是React-Redux提供的核心組件,二者看起來功能同樣,都是幫助容器組件獲取store中的數據,可是原理與功能卻不一樣。
Provider組件在全部組件的最外層,其接受store做爲參數,將store裏的state使用context屬性向下傳遞。部分源碼:
export default class Provider extends Component { getChildContext() { return { store: this.store } } constructor(props, context) { super(props, context) this.store = props.store } render() { const { children } = this.props return Children.only(children) } }
利用context這個屬性,Provider全部子組件都可以拿到這個屬性。
connect實現的功能是將須要關聯store的組件和store的dispatch等數據混合到一塊,這塊就是一個高階組件典型的應用:
import hoistStatics from 'hoist-non-react-statics' export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { // ... return function wrapWithConnect(WrappedComponent) { // ... class Connect extends Component { // ... render() { // ... if (withRef) { this.renderedElement = createElement(WrappedComponent, { ...this.mergedProps, ref: 'wrappedInstance' }) } else { this.renderedElement = createElement(WrappedComponent, this.mergedProps ) } return this.renderedElement } } // ... return hoistStatcis(Connect, WrappedComponent); } }
仍是先從他的四個參數提及:
connect 的第一個參數定義了咱們須要從 Redux 狀態樹中提取哪些部分看成 props 傳給當前組件。通常來講,這也是咱們使用 connect 時常常傳入的參數。事實上,若是不傳入這個參數,React 組件將永遠不會和 Redux 的狀態樹產生任何關係。具體在源代碼中的表現爲:
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { const shouldSubscribe = Boolean(mapStateToProps) // ... class Connect extends Component { // ... trySubscribe() { if (shouldSubscribe && !this.unsubscribe) { this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) this.handleChange() } } // ... } }
mapStateToProps
會訂閱 Store,每當state
更新的時候,就會自動執行,從新計算 UI 組件的參數,從而觸發 UI 組件的從新渲染。
mapStateToProps
的第一個參數老是state
對象,還可使用第二個參數,表明容器組件的props
對象。
這塊的源碼相對較簡單:
const mapState = mapStateToProps || defaultMapStateToProps class Connect extends Component { computeStateProps(store, props) { if (!this.finalMapStateToProps) { return this.configureFinalMapState(store, props) } const state = store.getState() const stateProps = this.doStatePropsDependOnOwnProps ? this.finalMapStateToProps(state, props) : this.finalMapStateToProps(state) if (process.env.NODE_ENV !== 'production') { checkStateShape(stateProps, 'mapStateToProps') } return stateProps } configureFinalMapState(store, props) { const mappedState = mapState(store.getState(), props) const isFactory = typeof mappedState === 'function' this.finalMapStateToProps = isFactory ? mappedState : mapState this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1 if (isFactory) { return this.computeStateProps(store, props) } if (process.env.NODE_ENV !== 'production') { checkStateShape(mappedState, 'mapStateToProps') } return mappedState } }
這塊原理很簡單,進行一些參數校驗,判斷第一個參數mapStateToProps返回值是否爲function,若是是遞歸調用,不是的話算出返回值。若是沒傳這個參數,默認給{}。
咱們可能會疑惑爲何傳給 connect 的第一個參數自己是一個函數,react-redux 還容許這個函數的返回值也是一個函數呢?
簡單地說,這樣設計能夠容許咱們在 connect 的第一個參數裏利用函數閉包進行一些複雜計算的緩存,從而實現效率優化的目的
當咱們使用的時候:
const mapStateToProps = (state, props) => ({ home: state.home, layout: state.layout });
使用ownProps
做爲參數後,若是容器組件的參數發生變化,也會引起 UI 組件從新渲染
人如其名,它接受 store 的 dispatch 做爲第一個參數,同時接受 this.props 做爲可選的第二個參數。利用這個方法,咱們能夠在 connect 中方便地將 actionCreator 與 dispatch 綁定在一塊兒(利用 bindActionCreators 方法),最終綁定好的方法也會做爲 props 傳給當前組件。這塊的源碼與mapStateToProps同樣,就不貼了。
bindActionCreator
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) }
前兩個參數返回的對象,都要跟組件自身的props merge一下,造成一個新的對象賦值給對應組件,咱們能夠在這一步作一些處理,這個參數就是幹這個的,該參數簽名:
mergeProps(stateProps, dispatchProps, ownProps): props
默認狀況若是沒傳該參數,返回Object.assign(ownProps, stateProps, dispatchProps)
。
若是指定這個參數,能夠定製 connector 的行爲。
pure = true
] (Boolean): 若是爲 true,connector 將執行 shouldComponentUpdate
而且淺對比 mergeProps
的結果,避免沒必要要的更新,前提是當前組件是一個「純」組件,它不依賴於任何的輸入或 state 而只依賴於 props 和 Redux store 的 state。默認值爲 true。 withRef = false
] (Boolean): 若是爲 true,connector 會保存一個對被包裝組件實例的引用,該引用經過 getWrappedInstance()
方法得到。默認值爲 false。 這個connect組件還幹了一件事,狀態緩存判斷。當store變了的時候,先後狀態判斷,若是狀態不等,更新組件,而且完成事件分發。
上面講了大量的函數源碼,這麼些函數之間的關係:
初始化階段:
更新數據階段:
redux核心函數大量使用了匿名函數和閉包來實現數據共享和狀態同步。
使用函數柯里化s實現參數複用,本質上是下降通用性,提升適用性。
對於state這種核心狀態使用getState()計算出新的state,而不是直接返回一個state對象。
使用觀察者訂閱者模式實現數據響應。
平時開發不常接觸的api實現Provider與Connect通訊。