Redux
對於 Redux 結構與程式碼實現的剖析,以及專案中的高階用法,不進行對於API的介紹
createStore
- 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();
}
});
};
複製程式碼