對redux的認識(原始碼深度解讀)
用redux寫過一些小專案,感覺有段時間不用快要忘記。所以寫下我對redux的理解(和react-redux結合使用)。作為筆記。redux版本是3.6.0
。react-redux版本是5.0.4
。
首先開啟github的redux專案,在原始碼中,主要有以下檔案:
這也是redux的組成部分。utils
資料夾只有一個warning.js
,用來列印錯誤資訊。index.js
用來匯出相關內容。關鍵的是其他五個檔案。
applyMiddleware.js
,bindActionCreator.js
,combineReducers.js
,compose.js
,createStore.js
。
一個redux的流程是這樣的。
1、在View上,使用者會做出一個動作(比如點選)。
2、Store通過其方法dispatch這個動作,這個動作往往是一個物件({type: 'click', payload: {}}
),或者是一個函式(需要redux-thunk
或其他中介軟體處理)。對於Action
,一般都是大寫並且宣告為常量。
store.dispatch({
type: 'SELECT_BOOKNAME',
payload: {
text: 'react'
}
})
3、Reducers會收到這個動作,並進行相應的處理。
import { SELECT_BOOKNAME } from '../actions' ;
const selectedBookName = (state = 'java', action) => {
switch (action.type) {
case SELECT_BOOKNAME:
return action.payload.text;
default:
return state;
}
}
export default selectedBookName;
reduce處理會改變應用的state(redux應用中,只有一個store)。如上action.payload.text
。預設情況下state的值為java
,現在變成了react
state.selectedBookName
屬性。
4、state改變了,一般而言,頁面(View)會重新render。依賴於state的資料也會改變。
下面是原始碼分析
compose
本來把compose放在後面,想想還是提前了。
compose是一個輔助函式,效果很簡單,它只是簡化了深層巢狀呼叫函式的問題。compose(f, g)
的行為等價於(...args) => f(g(...args))
。簡單的說,compose(f, g)
是一個函式,接受的引數會傳給函式g
,函式g
執行,將函式返回的結果作為函式f
的引數。然後執行函式f
。compose(f, g)
的返回值即函式f
執行後的返回值。
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
這裡主要使用的是Array.prototype.reduce函式。reduce(function(acc, currentValue, currentIndex, arr), initialValue)
。其中initialValue
是可選的。如果提供了initialValue
,那麼acc
的值是initialValue
,currentValue
的值是陣列的第一個元素。如果沒提供initialValue
,那麼acc
的值是陣列的第一個值,currentValue
的值是陣列的第二個值。
Array.prototype.reduce
函式中的引數函式(callback)將會從左到右處理陣列的每一個值。然後將最終的結果返回。在這裡,callback
返回一個函式。
(...args) => a(b(...args));
所以compose
函式的返回值也是一個函式(A),其接受一個或多個引數,然後將引數傳入到compose
函式中最後一個引數執行。執行的結果作為compose
函式中倒數第二個函式的引數。依次執行。
compose(fn1, fn2, fn3)(1,2,3);
// 等價於
fn1(fn2(fn3(1,2,3)))
上述程式碼將引數1,2,3
作為fn3的引數。fn3(1,2,3)
執行的結果作為fn2
的引數…
createStore
去掉了註釋和輔助函式以及store的一些函式的具體的實現(dispatch、getState、subscribe、replaceReducer)。將精力全部集中到createStore這個函式。
// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState) // 注1
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
function ensureCanMutateNextListeners() {}
function subscribe(listener) {}
function dispatch(action) {}
function replaceReducer(nextReducer) {}
function observable() {}
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
在建立store的時候,我們可以傳入初始狀態(preloadedState
),但這不是必須的。所以一開始會判斷。
enhancer一般是applyMiddleware
或者是compose
函式。通過它可以使用一些第三方元件,比如redux-logger
,redux-thunk
等。這個引數也是可選的。
一般我們建立的store是這樣的。
const store = createStore(
reducers,
preloadState,
compose(
applyMiddleware(...middleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
// 或者是
const store = createStore(
reducers,
preloadState,
applyMiddleware(...middleware)
)
不管如何,如果有enhancer,那麼就會轉而執行enhancer(注1)。假設這裡是applyMiddleware
。執行applyMiddleware
並傳入引數。並把函式本身傳入作為引數。然後執行enhancer(createStore)
(返回的肯定是一個函式,從下文applyMiddleware
可以看到)並傳入(reducer, preloadedState)
。
return enhancer(createStore)(reducer, preloadedState) // 注1
applyMiddleware
// applyMiddleware.js
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => { // 注2
const store = createStore(reducer, preloadedState, enhancer) // 注3
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
注2這裡不是很容易理解的,需要對照上文的createStore
。
return enhancer(createStore)(reducer, preloadedState) // 注1
return (createStore) => (reducer, preloadedState, enhancer) => { // 注2
相互對照,方便理解。那麼流程就是在createStore
的時候,如果有enhancer(如applyMiddleware
),就先執行enhancer,並傳入createStore
函式本身以及它的引數reducer, preloadState
。
再檢視注3,也就是applyMiddleware
的開始部分。因為applyMiddleware
的引數是一個個元件,對於這些元件,執行createStore
函式,並傳入第一次createStore傳下來的兩個引數。所以流程就是createStore->applyMiddleware->createStore
。由於這次的enhancer是undefined
,所以在createStore.js
中會繼續往下執行,返回一個store
。繞了個彎。
const store = createStore(reducer, preloadedState, enhancer)
然後從注3往下執行。關鍵來了。先插述點東西。
————————————————————————-
one:
以中介軟體的最簡單方式為例,建立一箇中間件通常是這樣的(如logger中介軟體):
const logger = store => next => action => { // next相當於dispatch
console.log('dispatching', action);
let result = next(action); // 相當於dispatch(action),返回的結果仍然是action
console.log('next state', store.getState()); // 注4
return result; // 返回action,供其他中介軟體處理
}
————————————————————————-
插述完畢。
之前說到從注3往下執行。接下來分析這一塊程式碼:
// applyMiddleware.js
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
middlewares
是所有中介軟體組成的陣列。呼叫map
方法,傳入中介軟體(是一個函式,比如上述的logger中介軟體),並執行這個函式,傳入middlewareAPI
變數(相當於store)。
const logger = store => next => action => { // next相當於dispatch,往下文看
console.log('dispatching', action);
let result = next(action); // 相當於dispatch(action),返回的結果仍然是action
console.log('next state', store.getState()); // 注4
return result; // 返回action,供其他中介軟體處理
}
再看看吧,logger是middleware
,middlewareAPI
相當於store,所以可以在注4呼叫store.getState()
方法。
middlewares.map
返回值是一個數組,賦值給chain
,陣列的每一項是函式。即中介軟體(logger)執行後返回的函式(fn = next => action => {}
)。
然後增強dispatch。
dispatch = compose(...chain)(store.dispatch)
剛說的chain
中的每一項是個陣列,並且compose
的作用就是從右到左執行它的函式引數。所以需要執行fn = next => action => {}
這個函式,這個函式的返回值還是一個函式(爽不爽!!!,函式是這樣的:fn = action => {}
,這個函式能接受action
作為引數,當然是dispatch
了)。對於fn = next => action => {}
傳入store.dispatch
,所以next
就是store.dispatch
(上文也提到)。然後將返回的函式next
再傳遞給倒數第二個引數(從右往左),然後一直往左,直到第一個。想象一下:
chain = [returnedLogger1, returnedLogger2, returnedLogger3];
首先store.dispatch
傳遞給returnedLogger3
,返回值是一個函式,fn = action => {}
,能接受action
作為引數,其實際上還是store.dispatch
。所以就是store.dispatch
一直作為引數從右向左傳遞,直到compose
最左邊的函式引數執行完畢,返回的store.dispatch
賦值給最初的dispatch
。666666版本的dispatch出現了!!!
然後就是applyMiddleware.js
內層函式的返回值了。
return {
...store,
dispatch
}
...store
表示解構store
這個物件(其中有dispatch,subscribe,getState等),其中dispatch
會被增強版本的dispatch
覆蓋。
至此,applyMiddleware.js
分析完畢。
下面是一個測試:
const logger1 = store => next => action => {
console.log(111);
let result = next(action);
console.log(222);
return result;
}
const logger2 = store => next => action => {
console.log(111111);
let result = next(action);
console.log(222222);
return result;
}
const logger3 = store => next => action => {
console.log(111111111);
let result = next(action);
console.log(222222222);
return result;
}
const configureStore = (preloadState={aaa: 'aaaa'}) => {
const store = createStore(
reducers,
preloadState,
compose(
applyMiddleware(thunk, logger1, logger2, logger3),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
return store;
}
以applyMiddleware(thunk, logger1, logger2, logger3),
這種形式,打印出的結果是:
111, 111111, 111111111, 222222222, 222222, 222
combineReducers
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const 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') { // 小的reducer肯定是function啊
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 忽略中間這些異常處理吧,比如preloadState中的key在reducers(引數reducer,是一個物件)中不存在。
return function combination(state = {}, action) { // 注5
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
一般而言,finalReducers
和reducers
是相同的(見上述程式碼)。從異常處理下面開始分析。finalReducerKeys
是一個數組。
// 例子reducers
export default combineReducers({
postByBookName,
selectedBookName,
user
})
比如上面這個reducers,finalReducerKeys
的值是:
["postByBookName","selectedBookName","user"]
由於在createStore
的時候傳入了大的reducer
,所以store.dispatch
一個action
的時候大的reducer
會處理這個action
。
createStore.js
中有兩行程式碼:
let currentReducer = reducer
let currentState = preloadedState
// 下面這行程式碼在store.dispatch函式中
currentState = currentReducer(currentState, action)
反映在上述程式碼就是執行注5的函式。
return function combination(state = {}, action) { // 注5
關鍵是這5行程式碼,以上述例子reducers
其中的selectedBookName
為例。
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
key
是字串selectedBookName
。
reducer
是selectedBookName
對應的reducer。我的是:
const selectedBookName = (state = 'java', action) => {
switch (action.type) {
case SELECT_BOOKNAME:
return action.payload.text;
default:
return state;
}
}
export default selectedBookName;
previousStateForKey
是undefined
。
nextStateForKey
是selectedBookName
函式執行的結果,如果匹配上了返回action.payload.text
,假如是”react”。
nextState["selectedBookName"] = "react"
。
此時由於previousStateForKey
不等於nextStateForKey
,所以hasChanged
的值為true。返回nextState
。
至此,combineReducers.js
分析完畢。
bindActionCreators
這是個輔助函式。比如:
const test = () => {
type: 'test',
}
以前我們的用法是dispatch(test())
,使用了這個輔助函式,我們只需要test()
。就是少寫了dispatch
這麼回事。一般情況下我不會用到。
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
} else {
warning(`bindActionCreators expected a function actionCreator for key '${key}', instead received type '${typeof actionCreator}'.`)
}
}
return boundActionCreators
}
上述原始碼分為兩種情況,一種是typeof actionCreator === 'function'
,就是我舉例的那種。一種是actionCreator
是一個物件。通常在這種情況出現:
import * as Actions from '../actions'
這樣Actions
是一個物件,每個import
出的函式就是Actions
的一個屬性值。然後向一種一樣的處理。通常我不會使用這個輔助函式,當我使用react-redux
的時候,我可能會用到。
import * as CounterActions from '../actions'
const mapStateToProps = (state) => ({
counter: state.counter
})
function mapDispatchToProps(dispatch) {
return bindActionCreators(CounterActions, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
然後我直接呼叫CounterActions.myAction()
就直接dispatch
了。
結語
花了一晚上時間,梳理了redux的結構,加深了理解。對原始碼進行了細緻的分析。如果有更多的想要和我交流,可以加我:
QQ: 1227620310
微信: a127620310