摘要:在得到新的狀態后,依次調用所有的監聽器,通知狀態的變更。執行完后,獲得數組,它保存的對象是第二個箭頭函數返回的匿名函數。部分源碼利用這個屬性,所有子組件均可以拿到這個屬性。
Redux使用中的幾個點:
Redux三大設計原則
Create Store
Redux middleware
combineReducer
Provider與Connect
Redux流程梳理
Redux設計特點
1. Redux三大設計原則 1. 單一數據源在傳統的 MVC 架構中,我們可以根據需要創建無數個 Model,而 Model 之間可以互相監聽、觸發事件甚至循環或嵌套觸發事件,這些在 Redux 中都是不允許的。因為在 Redux 的思想里,一個應用永遠只有唯一的數據源。
實際上,使用單一數據源的好處在于整個應用狀態都保存在一個對象中,這樣我們隨時可以提取出整個應用的狀態進行持久化(比如實現一個針對整個應用的即時保存功能)。此外,這樣的設計也為服務端渲染提供了可能。
在 Redux 中,我們并不會自己用代碼來定義一個 store。取而代之的是,我們定義一個 reducer,它的功能是根據當前觸發的 action 對當前應用的狀態(state)進行迭代,這里我們并沒有直接修改應用的狀態,而是返回了一份全新的狀態。
Redux 提供的 createStore 方法會根據 reducer 生成 store。最后,我們可以利用 store. dispatch
方法來達到修改狀態的目的。
在 Redux 里,我們通過定義 reducer 來確定狀態的修改,而每一個 reducer 都是純函數,這意味著它沒有副作用,即接受一定的輸入,必定會得到一定的輸出。
這樣設計的好處不僅在于 reducer 里對狀態的修改變得簡單、純粹、可測試,更有意思的是,Redux 利用每次新返回的狀態生成酷炫的時間旅行(time travel)調試方式,讓跟蹤每一次因為觸發 action 而改變狀態的結果成為了可能。
2.Create Store我們從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的返回值就是一個普通對象,里面有幾個常用的方法:
dispatch:就是我們最常用的dispatch方法,派發action。
getState:通過該方法,我們可以拿到當前狀態樹state。
replaceReducer:這個方法主要用于 reducer 的熱替換,下面介紹該方法。
subscribe:添加一個變化監聽器。每當 dispatch(action)的時候就會執行,state 樹中的一部分可能已經變化。
observable:觀察者模式,用于處理訂閱關系。
這里挑幾個方法介紹:
getState在完成基本的參數校驗之后,在 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。
subscribe在 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方法返回一個函數,調用這個函數就可以解除監聽。
dispatchdispatch是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 方法返回。
replaceReducerfunction 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 應用才能有條不紊地開始運作。
3.Redux middlewareIt 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 結構的好處主要有以下兩點。
易串聯:currying 函數具有延遲執行的特性,通過不斷 currying 形成的 middleware 可以累積參數,再配合組合(compose)的方式,很容易形成 pipeline 來處理數據流。
? 共享 store: 在 applyMiddleware 執行的過程中,store 還是舊的,但是因為閉包的存在,applyMiddleware 完成后,所有的 middleware 內部拿到的 store 是最新且相同的。
給 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名稱前面加上模塊前綴即可解決問題。
5. Provider與ConnectProvider與Connet組件都是React-Redux提供的核心組件,兩者看起來功能一樣,都是幫助容器組件獲取store中的數據,但是原理與功能卻不同。
ProviderProvider組件在所有組件的最外層,其接受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所有子組件均可以拿到這個屬性。
Connectconnect實現的功能是將需要關聯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); } }
還是先從他的四個參數說起:
1.mapStateToPropsconnect 的第一個參數定義了我們需要從 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 組件重新渲染
2.mapDispatchToProps人如其名,它接受 store 的 dispatch 作為第一個參數,同時接受 this.props 作為可選的第二個參數。利用這個方法,我們可以在 connect 中方便地將 actionCreator 與 dispatch 綁定在一起(利用 bindActionCreators 方法),最終綁定好的方法也會作為 props 傳給當前組件。這塊的源碼與mapStateToProps一樣,就不貼了。
bindActionCreator
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) }3.mergeProps
前兩個參數返回的對象,都要跟組件自身的props merge一下,形成一個新的對象賦值給對應組件,我們可以在這一步做一些處理,這個參數就是干這個的,該參數簽名:
mergeProps(stateProps, dispatchProps, ownProps): props
默認情況如果沒傳該參數,返回Object.assign(ownProps, stateProps, dispatchProps)。
4.options如果指定這個參數,可以定制 connector 的行為。
[pure = true] (Boolean): 如果為 true,connector 將執行 shouldComponentUpdate 并且淺對比 mergeProps 的結果,避免不必要的更新,前提是當前組件是一個“純”組件,它不依賴于任何的輸入或 state 而只依賴于 props 和 Redux store 的 state。默認值為 true。
[withRef = false] (Boolean): 如果為 true,connector 會保存一個對被包裝組件實例的引用,該引用通過 getWrappedInstance() 方法獲得。默認值為 false。
這個connect組件還干了一件事,狀態緩存判斷。當store變了的時候,前后狀態判斷,如果狀態不等,更新組件,并且完成事件分發。
6. Redux流程梳理上面講了大量的函數源碼,這么些函數之間的關系:
初始化階段:
createStore創建一個store對象
將store對象通過參數給Provider組件
Provider組件將store通過context向子組件傳遞
Connect組件通過context獲取到store,存入自己的state
componentDidMount里面訂閱store.subscribe事件
更新數據階段:
用戶事件觸發
actionCreator生成action交給dispatch
實際上交給了封裝后的中間層(compose(applyMiddleware(...)))
請求依次通過每個中間件,中間件通過next進行下一步
最后一個中間件將action交給store.dispatch
dispatch內部將action交給reducer執行
combineReducer將每個子reducer執行一遍算出新的state
dispatch內部調用所有訂閱事件
Connect組件handleChange事件觸發判斷新state和舊state是否===
并且判斷新的state是否與mapStateToProps shallowEqual
不等則setState觸發更新
7.Redux設計技巧匿名函數&&閉包使用
redux核心函數大量使用了匿名函數和閉包來實現數據共享和狀態同步。
函數柯里化使用
使用函數柯里化s實現參數復用,本質上是降低通用性,提高適用性。
核心狀態讀取是拷貝而不是地址
對于state這種核心狀態使用getState()計算出新的state,而不是直接返回一個state對象。
觀察者訂閱者是核心實現
使用觀察者訂閱者模式實現數據響應。
context這個api的使用
平時開發不常接觸的api實現Provider與Connect通信。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93471.html
摘要:歡迎關注源碼分析系列文章源碼分析之一源碼分析之二源碼分析之三源碼分析之四源碼分析之五文件算是非常簡單的一個文件了,該文件就實現一個目的以前這樣觸發一個,即,現在變成這樣觸發一個。目的很單純,簡化某個的調用。 歡迎關注redux源碼分析系列文章:redux源碼分析之一:createStore.jsredux源碼分析之二:combineReducers.jsredux源碼分析之三:bind...
摘要:歡迎關注源碼分析系列文章源碼分析之一源碼分析之二源碼分析之三源碼分析之四源碼分析之五文件對外暴露了一個函數,函數是的一個輔助性的函數,用于拆分里面的第一個參數函數。函數的返回值是一個函數,該函數是組合之后的一個標準的函數。 歡迎關注redux源碼分析系列文章:redux源碼分析之一:createStore.jsredux源碼分析之二:combineReducers.jsredux源碼分...
摘要:的中間件主要是通過模塊實現的。返回的也是一個對象這個其實就是,各個中間件的最底層第三層的哪個函數組成的圓環函數構成的這就是對源碼的一個整體解讀,水平有限,歡迎拍磚。后續的源碼解讀和測試例子可以關注源碼解讀倉庫 applyMiddleware源碼解析 中間件機制在redux中是強大且便捷的,利用redux的中間件我們能夠實現日志記錄,異步調用等多種十分實用的功能。redux的中間件主要是...
摘要:實現一個先不考慮中間件,實現一個簡潔的實現是最主要的一個了,通過可以創建一個用來存放應用中所有的,一個應用只能有一個。方法是用來把每一個用方法包裹一下,因為可能只是返回一個具有屬性的對象,只有用執行才有意義。正好可以利用的特性實現這個效果。 實現一個redux 先不考慮中間件,實現一個簡潔的redux 實現createStore createStore是redux最主要的一個API了,...
摘要:的返回值是函數,這個函數經調用,傳入參數,之后會在中間件鏈上進行傳遞,只要保證每個中間件的參數是并且將傳遞給下一個中間件。 了解了Redux原理之后,我很好奇Redux中間件是怎么運作的,于是選了最常用的redux-thunk進行源碼分析。 此次分析用的redux-thunk源碼版本是2.2.0,redux源碼版本是3.7.2。并且需要了解Redux原理 redux中間件都是由redu...
閱讀 999·2019-08-30 15:55
閱讀 3440·2019-08-30 13:10
閱讀 1268·2019-08-29 18:45
閱讀 2347·2019-08-29 16:25
閱讀 2107·2019-08-29 15:13
閱讀 2421·2019-08-29 11:29
閱讀 551·2019-08-26 17:34
閱讀 1485·2019-08-26 13:57