從原始碼的入口檔案發現,其實 redux 最終就只是匯出了一個物件,物件中有幾個方法,程式碼如下:
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
複製程式碼
所以重點分析幾個方法:
createStore 方法
方法中定義的一些變數:
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
複製程式碼
這些變數會被 dispatch
或者別的方法引用,從而形成閉包。這些變數不會被釋放。
建立 srore
的方法最終返回的是一個物件。物件中含有比較重要的方法dispatch,subscribe,getState
。
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
複製程式碼
其中 createStore
的第三個引數是應用中介軟體來做一些增強操作的。
if (typeof enhancer !== 'undefined') { // 如果增強方法存在就對 createStore 進行增強
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
複製程式碼
subscribe 方法
其中 subscribe
用來註冊監聽方法,每一次註冊後會將監聽方法維護到陣列currentListeners
中,currentListeners
是 createStore
中的一個變數,由於被 subscribe
引用著所以形成了一個閉包。也就是通過閉包來維護狀態。
let currentListeners = []
複製程式碼
dispatch 方法
dispatch
方法用來分發 action
, 函式裡面會生成新的 currentState
, 會執行所有註冊了的函式。
核心程式碼:
try {
isDispatching = true
currentState = currentReducer(currentState, action) // 生成新的 state
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
} // 遍歷執行註冊函式
複製程式碼
getState
僅僅用來獲得當前的 state
:
function getState() {
return currentState
}
複製程式碼
combineReducers 函式
函式中定義的一些變數,
const finalReducers = {}
const finalReducerKeys = Object.keys(finalReducers)
複製程式碼
這個函式最後返回的是一個函式 combination
, 返回的函式中引用了 finalReducers
和 finalReducerKeys
,形成了閉包。
出於業務場景考慮,不同的模組採用不同的 reducer
進行處理,所以 reducer
函式有很多。這些 reducer
會遍歷執行。
每一次 dispatch
一個 action
的時候就會執行
currentState = currentReducer(currentState, action) // 生成新的 state
複製程式碼
這裡的 currentReducer
就是返回的 combination
函式。combination
函式中的核心程式碼:
function combination(state = {}, action) {
...
let hasChanged = false
// 每一次 reducer 執行的時候都會生成一個新的物件來作為新的 state
const nextState = {}
// 通過 for 迴圈遍歷 reducer
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
// 獲取當前的 state
const previousStateForKey = state[key]
// 執行相應的 reducer 後會生成新的 state
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// 給新的 state 賦值
nextState[key] = nextStateForKey
// 如果是一個簡單型別比如 string,number
// 如果前後值一樣就不會觸發改變
// 但如果 state 中某個值是一個物件,
// 儘管前後物件中的值一樣,但是引用地址變化,還是會觸發改變
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 所以如果簡單值沒有變化並且沒有物件的引用地址改變就會返回原來的 state
return hasChanged ? nextState : state
}
複製程式碼
結合 react-redux
中向 redux
訂閱的方法發現
subscribe() {
const { store } = this.props // 這裡的 store 是 createStore 方法執行後返回的物件
this.unsubscribe = store.subscribe(() => { // 通過訂閱方法註冊監聽事件
const newStoreState = store.getState() // 獲取新的 state
if (!this._isMounted) {
return
}
// 通過使用函式替代物件傳入 setState 的方式能夠得到元件的 state 和 props 屬性可靠的值。
this.setState(providerState => {
// 如果值是一樣的就不會觸發更新
if (providerState.storeState === newStoreState) {
return null
}
return { storeState: newStoreState }
})
})
// Actions might have been dispatched between render and mount - handle those
const postMountStoreState = store.getState()
if (postMountStoreState !== this.state.storeState) {
this.setState({ storeState: postMountStoreState })
}
}
複製程式碼
在註冊的 listen
方法中會發現如果最 新的state和原來的state一樣
就不會觸發 setState
方法的執行,從而就不會觸發 render
。
applyMiddleware 使用中介軟體
原始碼:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => { // 接收 createStore 函式作為引數
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
} // 中介軟體函式接收的 API 引數,能夠獲取到當前的 state 和 createStore 函式的引數
// 所以這裡就向中介軟體函式中傳遞了引數
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 通過函式組合生成一個新的 dispatch 函式
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
} // 這裡返回的是最後生成的 store,相比不使用中介軟體的區別是對 dispatch 進行了增強。
}
}
複製程式碼
結合 createStore
中的原始碼:
return enhancer(createStore)(reducer, preloadedState)
複製程式碼
所以上面 applyMiddleware
中返回的函式就是這裡的 enhancer
方法,接收 createStore
作為引數。
(reducer, preloadedState)
對應著中介軟體中的 (...args)
。
react-redux
react-redux
通過提供 Provider
元件將 store
和整個應用中的元件聯絡起來。確保整個元件都可以獲得 store
, 這是通過 Context
來實現的。
Provider
元件最終渲染的元件:
render() {
const Context = this.props.context || ReactReduxContext
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
複製程式碼
其中 state
的定義如下:
const { store } = props
this.state = {
storeState: store.getState(),
store
}
複製程式碼
所以 Provider
給應用提供 store
的寫法如下,屬性名必須是 store
。
<Provider store={store}>
<Router />
</Provider>
複製程式碼
redux-thunk
redux-thunk
是一箇中介軟體,直接看中介軟體的原始碼是絕對不可能看明白的
。
中介軟體不是一個完整的個體。它是為了豐富或者擴充套件某個模組而出現的,其中會呼叫一些原來的模組的方法,所以如果不看源模組的對應的方法實現,根本無法理解。
所以要想看懂一箇中介軟體,必須結合源模組的程式碼一起看。
JavaScript 語言是傳值呼叫,它的 Thunk 函式含義有所不同。在 JavaScript 語言中,Thunk 函式替換的不是表示式,而是多引數函式,將其替換成單引數的版本,且只接受回撥函式作為引數。
如何讓 dispatch 分發一個函式,也就是 action creator??
dispatch
的引數只能是一個普通的物件,如果要讓引數是一個函式,需要使用中介軟體 redux-thunk
。
設計思想就是一種面向切面程式設計AOP,對函式行為的增強,也是裝飾模式的使用。
redux-thunk
原始碼:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製程式碼
如果只是用了 thunk
,那麼最終增強版的 dispatch
就是
action => {
// 當 dispatch 引數是一個函式的時候執行這裡
if (typeof action === 'function') {
// 這裡的 dispatch 就是最原始的 dispatch
// 所以 action 函式中可以直接使用引數 dispatch 和 getState 函式
return action(dispatch, getState, extraArgument);
}
return next(action); // 這裡的 next 是 store.dispatch
}
複製程式碼
非同步操作帶程式碼
非同步操作如果使用 action creator
, 則至少要送出兩個 Action:
- 使用者觸發第一個 Action,這個跟同步操作一樣,沒有問題;
- 在
action creator
函式中送出第二個Action
程式碼例項:
handleClick = () => {
const { dispatch } = this.props
dispatch(this.action); // 發出第一個 action(函式)
}
action = (dispatch, getState) => setTimeout(() => {
dispatch({ type: 'REQUESTSTART' })
}, 1000) // 發出第二個 action(普通物件)
複製程式碼
思考
非同步程式碼的處理一定要使用 redux-thunk
嗎?
非也。在觸發含有非同步程式碼的函式執行時,把 dispatch
函式作為一個引數傳給函式,然後這個非同步函式裡面在合適的時機呼叫 dispatch
發出 action
就行。
上面的非同步程式碼可改寫如下:
handleClick = () => {
const { dispatch } = this.props
this.action(dispatch);
}
action = dispatch => setTimeout(() => {
dispatch({ type: 'REQUESTSTART' })
}, 1000)
複製程式碼
不過相比 redux-thunk
有個缺陷就是不能獲取 getState
這個方法。