redux 原始碼解析與實際應用
-
createStore是一個函式,接收三個引數
recdcer,initState,enhancer
-
enhancer
是一個高階函式,用於增強create出來的store,他的引數是createStore
,返回一個更強大的store生成函式。(功能類似於middleware)。 -
我們mobile倉庫中的
storeCreator
其實就可以看成是一個enhancer,在createStore的時候將saga揉入了進去只不過不是作為createStore的第三個引數完成,而是使用middleware
完成。
function createStore(reducer, preloadedState, enhancer) { if (typeof enhancer !== 'undefined') { // createStore 作為enhancer的引數,返回一個被加強的createStore,然後再將reducer, preloadedState傳進去生成store return enhancer(createStore)(reducer, preloadedState); } // ...... return { dispatch: dispatch, subscribe: subscribe, getState: getState, replaceReducer: replaceReducer }; } 複製程式碼
-
-
applyMiddleware 與 enhancer的關係。
- 首先他們兩個的功能一樣,都是為了增強store
- applyMiddleware的結果,其實就是一個enhancer
function applyMiddleware() { // 將傳入的中介軟體放入middlewares for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) { middlewares[_key] = arguments[_key]; } // return了一個 enhancer函式,引數為createStore,內部對store進行了增強 return function (createStore) { return function () { // 將createStore的引數傳入createStore,並生成store var store = createStore.apply(undefined, args); // 增強 dispatch var _dispatch = compose.apply(undefined, chain)(store.dispatch); // return 一個被增強了dispatch的store return _extends({}, store, { dispatch: _dispatch }); }; }; } 複製程式碼
store
store有四個基礎方法: dispatch、subscribe、getState、replaceReducer
- store.dispatch (發起action)
function dispatch(action) { // 校驗 action 格式是否合法 if (typeof action.type === 'undefined') { throw new Error('action 必須有type屬性'); } // 不可以在 reducer 進行中發起 dispatch if (isDispatching) { throw new Error('Reducers may not dispatch actions.'); } try { // 標記 dispatch 狀態 isDispatching = true; // 執行相應的 reducer 並獲取新更新的 state currentState = currentReducer(currentState, action); } finally { isDispatching = false; } // 把上次subscribe時得到的新的監聽函式列表,賦值成為當前的監聽函式列表 var listeners = currentListeners = nextListeners; // dispatch 的時候會依次執行 nextListeners 的監聽函式 for (var i = 0; i < listeners.length; i++) { var listener = listeners[i]; listener(); } return action; } 複製程式碼
- store.subscribe (用於監聽 store 的變化)
function subscribe(listener) { // 如果是在 dispatch時註冊subscribe,丟擲警告 if (isDispatching) { throw new Error('......'); } // 將監聽函式放入一個佇列 nextListeners.push(listener); // return 一個函式,用於登出監聽事件 return function unsubscribe() { // 同樣的,不能再 dispatch 時進行登出操作 if (isDispatching) { throw new Error('......'); } var index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); }; } 複製程式碼
- store.getState(獲取當前的state)
function getState() { if (isDispatching) { throw new Error('不允許在reducer執行中獲取state'); } // retuen 上次 dispatch 時所更新的 currentState return currentState; } 複製程式碼
- store.replaceReducer(提換當前的reducer)
function replaceReducer(nextReducer) { // 檢驗新的 reducer 是否是一個函式 if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.'); } // 替換掉當前的 reducer currentReducer = nextReducer; // 發起一次新的 action, 這樣可以使 sisteners 函式列表執行一遍,也可以更新一遍 currentState dispatch({ type: ActionTypes.REPLACE }); } 複製程式碼
combineReducers(組合reducer)
用於將多個reducer組合成一個reducer,接受一個物件,物件的每個屬性即是單個reducer,各個reducer的key需要和傳入該reducer的state引數同名。
function combineReducers(reducers) { // 所有傳入 reducers 的 key var reducerKeys = Object.keys(reducers); var finalReducers = {}; // 遍歷reducerKeys,將合法的 reducers 放入 finalReducers for (var i = 0; i < reducerKeys.length; i++) { var key = reducerKeys[i]; if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; } } // 可用的 reducers的 key var finalReducerKeys = Object.keys(finalReducers); var unexpectedKeyCache = void 0; { unexpectedKeyCache = {}; } var shapeAssertionError = void 0; // 將每個 reducer 都執行一遍,檢驗返回的 state 是否有為undefined的情況 try { assertReducerShape(finalReducers); } catch (e) { shapeAssertionError = e; } // return 一個組合過的 reducer 函式,返回值為 state 是否有變化 return function combination() { var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var action = arguments[1]; // 如果有返回的state不合法的reducer,丟擲錯誤 if (shapeAssertionError) { throw shapeAssertionError; } { // 校驗 state 與 finalReducers 的合法性 var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache); if (warningMessage) { warning(warningMessage); } } var hasChanged = false; var nextState = {}; // 遍歷所有可用的reducer,將reducer的key所對應的state,代入到reducer中呼叫 for (var _i = 0; _i < finalReducerKeys.length; _i++) { var _key = finalReducerKeys[_i]; var reducer = finalReducers[_key]; // reducer key 所對應的 state,這也是為什麼 reducer 名字要與 state 名字相對應的原因 var previousStateForKey = state[_key]; // 呼叫 reducer var nextStateForKey = reducer(previousStateForKey, action); // reducer 返回了新的 state,呼叫store.getState時返回的就是他 nextState[_key] = nextStateForKey; // 新舊 state 是否有變化 ? hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; }; } 複製程式碼
bindActionCreators
其實就是改變action發起的方式,之前是dispatch的方式,用bindActionCreators將actionCreator包裝後,生成一個key為actionType,value為接受 payload 的函式 的物件,發起action的時候直接呼叫這裡面名為跟action的type同名的函式
- 它的核心其實就是將actionCreator傳入然後返回一個可以發起dispatch的函式,函式中的dispatch接受一個已經生成的action,和在使用它的時候傳入的playload
function bindActionCreator(actionCreator, dispatch) { return function () { return dispatch(actionCreator.apply(this, arguments)); }; } 複製程式碼
- 將多個 actionCreators 進行包裝,最終返回一個被包裝過的actionCreators
function bindActionCreators(actionCreators, dispatch) { // 如果傳入一個函式,說明只有一個,actionCreator,返回一個可以進行 dispatch 的函式 if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch); } if ((typeof actionCreators === 'undefined' ? 'undefined' : _typeof(actionCreators)) !== 'object' || actionCreators === null) { throw new Error('校驗actionCreators是否是物件'); } // 檢索出 actionCreators 的 key var keys = Object.keys(actionCreators); var boundActionCreators = {}; // 迴圈將 actionCreators 中的項用 bindActionCreator 包裝一遍,放入 boundActionCreators 物件中並return for (var i = 0; i < keys.length; i++) { var key = keys[i]; var actionCreator = actionCreators[key]; if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch); } } return boundActionCreators; } 複製程式碼
compose
將多個函式組合成一個,從右往左依次執行
function compose() { // 獲取傳入引數的對映 for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) { funcs[_key] = arguments[_key]; } // 如果引數為0,return 一個 所傳即所得的函式 if (funcs.length === 0) { return function (arg) { return arg; }; } // 如果只有一個,返回此引數 if (funcs.length === 1) { return funcs[0]; } // 使用 reduce 將所有傳入的函式組合為一個函式,每一次執行reduce,a作為前一個函式都會被這個return的函式重新賦值 return funcs.reduce(function (a, b) { // 每次執行 reduce 都會返回這個函式,這個函式裡返回的前一個函式接受下一個函式的返回值作為引數 return function () { return a(b.apply(undefined, arguments)); }; }); } 複製程式碼
applyMiddleware (增強dispatch)
其實applyMiddleware就是將傳入的中介軟體進行組合,生成了一個接受 createStore為引數的函式(enhancer)。
// applyMiddleware將傳入的中介軟體組合成一個enhancer // 然後再傳入createStore改造成一個增強版的createStore // 最後傳入reducer 和 initialState 生成 store。 const store = applyMiddleware(...middlewares)(createStore)(reducer, initialState); // 其實跟這樣寫沒什麼區別 const store = createStore(reducer, initialState, applyMiddleware(...middlewares)); 複製程式碼
- 程式碼分析
function applyMiddleware() { // 將傳入的中介軟體組合成一個數組 for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) { middlewares[_key] = arguments[_key]; } // 返回一個接受 createStore 為引數的函式,也就是 enhancer return function (createStore) { // 其實這就是最終返回的被增強的 createStore return function () { for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } // 生成一個 store var store = createStore.apply(undefined, args); // 宣告一個_dispatch,用於替換 store 的 dispatch var _dispatch = function dispatch() { throw new Error('不允許在構建中介軟體時進行排程'); }; // 返回一個middlewareAPI,下一步將會被帶入中介軟體,使得每一箇中間件中都會有 getState 與 dispatch (例如redux-thunk) // 這裡面的 dispatch中,將會執行_dispatch(被增強的dispatch) var middlewareAPI = { getState: store.getState, dispatch: function dispatch() { return _dispatch.apply(undefined, arguments); } }; // 每一箇中間件都執行一遍 middlewareAPI var chain = middlewares.map(function (middleware) { return middleware(middlewareAPI); }); // 將 chain 用compose進行組合,所以傳入的中介軟體依賴必須是倒序的 // 並傳入 store.dispatch,生成一個被增強的 dispatch _dispatch = compose.apply(undefined, chain)(store.dispatch); // 生成 store, 使用 _dispatch 替換 store 原始的 dispatch return _extends({}, store, { dispatch: _dispatch }); }; }; } 複製程式碼
結合中介軟體redux-thunk
感受一下applyMiddleware
redux-thunk 可以使dispatch接受一個函式,以便於進行非同步操作
import { createStore, applyMiddleware } from 'redux'; import reduxThunk from 'redux-thunk'; import reducer from './reducers'; const store = createStore( reducer, {}, applyMiddleware(reduxThunk), ); 複製程式碼
- 原始碼
function createThunkMiddleware(extraArgument) { // reuturn 一個接受dispatch, getState的函式, // 這個函式返回的函式又接受上一個中介軟體的返回值,也就是被上一個中介軟體包裝過的dispatch // 如果接受的action是個函式,那麼就將dispatch, getState傳進去 return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); export default thunk; 複製程式碼
- 如果把thunk跟applyMiddleware組裝起來,就是這樣的
function applyMiddleware() { ... var middlewareAPI = { getState: store.getState, dispatch: function dispatch() { return _dispatch.apply(undefined, arguments); } }; var chain = middlewares.map(function () { // 這是middleware將middlewareAPI傳進去後return的函式 return function(next) { return function(action) { if (typeof action === 'function') { return action(dispatch, getState); } return next(action); } } }); // 將store.dispatch,也就是next傳進去 _dispatch = compose.apply(undefined, chain)(store.dispatch); } 複製程式碼
react-redux
用於繫結react 與 redux,其主要提供了兩個功能
Provider
用於包裝元件樹,將store傳入context中,使其子節點都可以拿到store,不需要一級一級的往下傳。
class Provider extends Component { // 將 store 放入 context 中 getChildContext() { return { store: this.store} } constructor(props, context) { super(props, context) this.store = props.store; } render() { return Children.only(this.props.children) } } 複製程式碼
connect
connect 用於state與容器元件之間的繫結。
connect 接受三個引數mapStateToProps, mapDispatchToProps, mergeProps
用於定義需要傳入容器元件的state與dispatch,然後return一個接受容器元件的函式(高階元件),這個高階函式會對將組合好的props混入進容器元件。
var containerComponent = connect(mapStateToProps,mapDispatchToProps)(someComponent); ReactDOM.render( <Provider store={store}> <HashRouter> <div> <Route exact path="/" component={containerComponent} /> </div> </HashRouter> </Provider>, document.querySelector('.doc') ); 複製程式碼
-
connect 接收的引數
- mapStateToProps 返回需要傳入容器元件的 state 的函式
const mapStateToProps = (state) => { return { stateName: state[stateName], }; } 複製程式碼
- mapDispatchToProps 返回需要傳入容器元件dispatch的函式 or 物件(如果是物件的話傳入的需要是個actionCreator,因為connect 內會用 bindActionCreators 將這個物件包裝)
// mapDispatchToProps 是個函式 const mapDispatchToProps = (dispatch) => { return { dispatchName: (action) => { dispatch(action); }, }; } // or 當 mapDispatchToProps 是個物件時原始碼中的處理 export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { return (mapDispatchToProps && typeof mapDispatchToProps === 'object') ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch)) : undefined } 複製程式碼
- mergeProps 規定容器元件props合併方式的函式
// 預設是將 mapStateToProps, mapDispatchToProps 與元件自身的props進行 merge const mergeProps = (stateProps, dispatchProps, ownProps) => { return { ...ownProps, ...stateProps, ...dispatchProps }; } 複製程式碼
-
connect 內的核心函式
-
finalPropsSelectorFactory(dispatch, {...options})
return 一個
pureFinalPropsSelector
函式,這個函式接受兩個引數,(state, props)並返回一個 mergeProps, 他將會在高階元件wrapWithConnect
中使用並傳入store.getState()和props,並以此對比當前的 props 以決定在shouldComponentUpdate
時是否需要更新 -
connectAdvanced(finalPropsSelectorFactory)
return 一個接受 容器元件為引數的高階元件(wrapWithConnect)。 wrapWithConnect需要的變數與屬性,這也就是
connect
最終 return 的結果。 -
wrapWithConnect(WrappedComponent)
這個就是被
connectAdvanced
返回的高階元件,其接受一個容器元件作為引數,在內部建立一個Connect
元件並在 render 的時候將整合好的 props 傳入 容器元件。 -
hoistStatics(a, b)將 b 的屬性複製到 a
用於在包裝元件的時候,將傳入的容器元件內的屬性都複製到 Connect 元件
function hoistNonReactStatics(targetComponent, sourceComponent, blacklist) { // 如果傳入的 b 是字串,直接return a if (typeof sourceComponent !== 'string') { // 層層遞迴,直到拿到 sourceComponent 的建構函式 var inheritedComponent = Object.getPrototypeOf(sourceComponent); if (inheritedComponent && inheritedComponent !== Object.getPrototypeOf(Object)) { hoistNonReactStatics(targetComponent, inheritedComponent, blacklist); } // b 的所有自有屬性的 key ,包括 Symbols 屬性 var keys = Object.getOwnPropertyNames(sourceComponent); keys = keys.concat(Object.getOwnPropertySymbols(sourceComponent)); // 過濾掉某些屬性,並將 b 的屬性複製給 a for (var i = 0; i < keys.length; ++i) { var key = keys[i]; if (!REACT_STATICS[key] && !KNOWN_STATICS[key] && (!blacklist || !blacklist[key])) { var descriptor = Object.getOwnPropertyDescriptor(sourceComponent, key); Object.defineProperty(targetComponent, key, descriptor); } } // return 一個被添加了 b 的屬性的 a return targetComponent; } return targetComponent; } 複製程式碼
-
- connect 原始碼分析
function connect( mapStateToProps, mapDispatchToProps, mergeProps){ // 對傳入的引數進行型別校驗 與 封裝 const initMapStateToProps = match(mapStateToProps, defaultMapStateToPropsFactories, 'mapStateToProps') const initMapDispatchToProps = match(mapDispatchToProps, defaultMapDispatchToPropsFactories, 'mapDispatchToProps') const initMergeProps = match(mergeProps, defaultMergePropsFactories, 'mergeProps') // return 一個接受 容器元件 為引數的高階元件(wrapWithConnect) return connectAdvanced(finalPropsSelectorFactory) } // 接受的其實是 `finalPropsSelectorFactory` function connectAdvanced(selectorFactory) { const storeKey = 'store'; // 用於說明訂閱物件 const subscriptionKey = storeKey + 'Subscription'; // 定義 contextTypes 與 childContextTypes 用於返回的高階函式裡的包裝元件 Connect const contextTypes = { [storeKey]: storeShape, [subscriptionKey]: subscriptionShape, } const childContextTypes = { [subscriptionKey]: subscriptionShape, } // 返回一個高階元件 return function wrapWithConnect(WrappedComponent) { // 這是一個接受真假 與 提示語 並丟擲錯誤的方法,這裡用來校驗傳入的是否是個函式 invariant(typeof WrappedComponent == 'function', `You must pass a component to the function`) // 將要傳入 finalPropsSelectorFactory 的 option const selectorFactoryOptions = { getDisplayName: name => `ConnectAdvanced(${name})`, methodName: 'connectAdvanced', renderCountProp: undefined, shouldHandleStateChanges: true, storeKey: 'store', withRef: false, displayName: getDisplayName(WrappedComponent.name), wrappedComponentName:WrappedComponent.displayName || WrappedComponent.name, WrappedComponent } // 用於生成一個 selector,用於Connect元件內部的更新控制 function makeSelectorStateful(sourceSelector, store) { // wrap the selector in an object that tracks its results between runs. const selector = { // 比較 state 與 當前的selector的props,並更新selector // selector 有三個屬性: // shouldComponentUpdate: 是否允許元件更新更新 // props: 將要更新的props // error: catch 中的錯誤 run: function runComponentSelector(props) { try { const nextProps = sourceSelector(store.getState(), props) if (nextProps !== selector.props || selector.error) { selector.shouldComponentUpdate = true selector.props = nextProps selector.error = null } } catch (error) { selector.shouldComponentUpdate = true selector.error = error } } } return selector } // 最終 return 的元件,用於包裝傳入的WrappedComponent class Connect extends Component { constructor(props, context) { super(props, context) this.store = props['store'] || context['store'] this.propsMode = Boolean(props['store']) // 校驗是否傳入了 store invariant(this.store, `Could not find store in either the context') this.initSelector() this.initSubscription() } componentDidMount() { // 會把 onStateChange 掛載到對store的訂閱裡 // 內部呼叫了 store.subscribe(this.onStateChange) this.subscription.trySubscribe() // 更新一遍 props this.selector.run(this.props) if (this.selector.shouldComponentUpdate) this.forceUpdate() } // 每次更新 props 都去對比一遍 props componentWillReceiveProps(nextProps) { this.selector.run(nextProps) } // 根據 selector 來進行元件更新的控制 shouldComponentUpdate() { return this.selector.shouldComponentUpdate } // 初始化 selector,用於元件props更新的控制 initSelector() { // 用於比較state與props的函式。並返回 merge 後的props const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) this.selector = makeSelectorStateful(sourceSelector, this.store) this.selector.run(this.props) } // 初始化訂閱模型: this.subscription initSubscription() { // 定義需要訂閱的資料來源,並將其傳入 Subscription 生成一個 subscription 物件 const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey] this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this)) this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription) } // 資料的監聽函式 onStateChange() { this.selector.run(this.props) } // 將 selector.props 傳入到傳入的元件 render() { const selector = this.selector selector.shouldComponentUpdate = false return createElement(WrappedComponent, selector.props) } } // 上面定義的 type 將作為 Connect 元件的屬性 Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName Connect.childContextTypes = childContextTypes Connect.contextTypes = contextTypes Connect.propTypes = contextTypes // 將傳入的元件的屬性複製進父元件 return hoistStatics(Connect, WrappedComponent) } } 複製程式碼
實用進階
動態載入 reducer
場景:
- 隨著專案的增大與業務邏輯越來越複雜,資料狀態與業務元件(WrappedComponent)也會越來越多,在初始化 store 的時候將所有reducer注入的話會使得資源很大
- 在入口將所有 reducer 注入的話,由於業務比較多,所以不一定都能用到,造成資源與效能浪費
方案:
利用redux.combineReducers
與store.replaceReducer
組合與更新reducer
// 初始化 store 的時候,將 reducer 記錄下來 // initReducer: 初始化時的 reducer 物件 var reducers = combineReducers(initReducer); const store = createStore( reducers, initState ); store.reducers = initReducer; // 載入子元件的時候,動態將新的 reducer 注入 function assignReducer(reducer) { // 合併新老 reducer const newReducer = Object.assign(store.reducers, reducer); // 經 combineReducers 組合後進行替換 store.replaceReducer(combineReducers(newReducer)); } 複製程式碼
時間旅行
場景:
- redux 是一個狀態管理器,我們的對 action 以及 reducer 的操作,其實最終的目的就是為了更新狀態,而時間旅行就是記錄我們的狀態更新的軌跡,並能回到某一個軌跡的節點。
- 比較容易理解的一個場景比如說翻頁,記錄每頁的資料,頁碼變化的時候直接恢復資料不用再次請求介面(並不適合實際業務場景)
- 時間旅行更大的作用其實在於我們可以監控狀態的變化,更方便的除錯程式碼
方案:
利用 store.subscribe, 監聽 dispatch 時記錄下此時的 狀態
const stateTimeline = [ initState ];// 記錄狀態的時間線 let stateIndex = 0;// 當前所處狀態的索引 // 當時間節點發生改變的時候,更替 state const reducer = (state, action) => { switch (action.type) { case 'CHANGE_STATE_INDEX': const currentState = action.playload.currentState; return currentState; default: return state; } }; const saveState = () => { // 將當前狀態push進時間線 stateTimeline.push(store.getState); stateIndex++; }; // 註冊監聽事件 store.subscribe(saveState); // 獲取某個時間節點的 state const getSomeNodeState = () => { return stateTimeline[stateIndex]; }; // 時間線控制器 const timeNodeChangeHandle = (someIndex) => { stateIndex = someIndex; store.dispatch({ type: 'CHANGE_STATE_INDEX', playload: { currentState: getSomeNodeState(); } }); }; 複製程式碼