同步自個人 博客react
好久以前就看過一遍 Redux
相關技術棧的源碼,最近在看書的時候發現有些細節已經忘了,並且發現當時的理解有些誤差,打算寫幾篇學習筆記。這是第一篇,主要記錄一下我對 Redux
、redux-thunk
源碼的理解。我會講一下大致的架構,和一些核心部分的代碼解釋,更具體的代碼解釋能夠去看個人 repo,後續會繼續更新 react-redux
,以及一些別的 redux
中間件的代碼和學習筆記。git
注意:本文不是單純的講 API
,若是不瞭解的能夠先看一下文檔,或者 google
一下 Redux
相關的基礎內容。github
在我看來,Redux 核心理念很簡單express
store
負責存儲數據redux
用戶觸發 action
數組
reducer
監聽 action
變化,更新數據,生成新的 store
promise
代碼量也不大,源碼結構很簡單:閉包
.src |- utils |- applyMiddleware.js |- bindActionCreators.js |- combineReducers.js |- compose.js |- createStore.js |- index.js
其中 utils
只包含一個 warning
相關的函數,這裏就不說了,具體講講別的幾個函數架構
這是入口函數,主要是爲了暴露 Redux
的 API
app
這裏有這麼一段代碼,主要是爲了校驗非生產環境下是否使用的是未壓縮的代碼,壓縮以後,由於函數名會變化,isCrushed.name
就不等於 isCrushed
if ( process.env.NODE_ENV !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed' ) { warning(...) )}
這個函數是 Redux
的核心部分了,咱們先總體看一下,他用到的思路很簡單,利用一個閉包,維護了本身的私有變量,暴露出給調用方使用的 API
// 初始化的 action export const ActionTypes = { INIT: '@@redux/INIT' } export default function createStore(reducer, preloadedState, enhancer) { // 首先進行各類參數獲取和類型校驗,不具體展開了 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') {...} if (typeof reducer !== 'function') {...} //各類初始化 let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false // 保存一份 nextListeners 快照,後續會講到它的目的 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } function getState(){...} function subscribe(){...} function dispatch(){...} function replaceReducer(){...} function observable(){...} // 初始化 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
下面咱們具體來講
這裏的 ActionTypes
主要是聲明瞭一個默認的 action
,用於 reducer
的初始化。
它的目的主要是保存一份快照,下面咱們就講講 subscribe
,以及爲何須要這個快照
目的是爲了添加一個監聽函數,當 dispatch action
時會依次調用這些監聽函數,代碼很簡單,就是維護了一個回調函數數組
function subscribe(listener) { // 異常處理 ... // 標記是否有listener let isSubscribed = true // subscribe時保存一份快照 ensureCanMutateNextListeners() nextListeners.push(listener) // 返回一個 unsubscribe 函數 return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false // unsubscribe 時再保存一份快照 ensureCanMutateNextListeners() //移除對應的 listener const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
這裏咱們看到了 ensureCanMutateNextListeners
這個保存快照的函數,Redux
的註釋裏也解釋了緣由,我這裏直接說說個人理解:因爲咱們能夠在 listeners
裏嵌套使用 subscribe
和 unsubscribe
,所以爲了避免影響正在執行的 listeners
順序,就會在 subscribe
和 unsubscribe
時保存一份快照,舉個例子:
store.subscribe(function(){ console.log('first'); store.subscribe(function(){ console.log('second'); }) }) store.subscribe(function(){ console.log('third'); }) dispatch(actionA)
這時候的輸出就會是
first third
在後續的 dispatch
函數中,執行 listeners
以前有這麼一句:
const listeners = currentListeners = nextListeners
它的目的則是確保每次 dispatch
時均可以取到最新的快照,下面咱們就來看看 dispatch
內部作了什麼。
dispatch
的內部實現很是簡單,就是將當前的 state
和 action
傳入 reducer
,而後依次執行當前的監聽函數,具體解析大概以下:
function dispatch(action) { // 這裏兩段都是異常處理,具體代碼不貼了 if (!isPlainObject(action)) { ... } if (typeof action.type === 'undefined') { ... } // 立一個標誌位,reducer 內部不容許再dispatch actions,不然拋出異常 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } // 捕獲前一個錯誤,可是會將 isDispatching 置爲 false,避免影響後續的 action 執行 try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } // 這就是前面說的 dispatch 時會獲取最新的快照 const listeners = currentListeners = nextListeners // 執行當前全部的 listeners for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }
這裏有兩點說一下個人見解:
爲何reducer
內部不容許再 dispatch actions
?我以爲主要是爲了不死循環。
在循環執行 listeners
時有這麼一段
const listener = listeners[i] listener()
乍一看以爲會爲何不直接 listeners[i]()
呢,仔細斟酌一下,發現這樣的目的是爲了不 this
指向的變化,若是直接執行 listeners[i]()
,函數裏的 this
指向的是 listeners
,而如今就是指向的 Window
。
獲取當前的 state
,代碼很簡單,就不貼了。
更換當前的 reducer
,主要用於兩個目的:1. 本地開發時的代碼熱替換,2:代碼分割後,可能出現動態更新 reducer的狀況
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } // 更換 reducer currentReducer = nextReducer // 這裏會進行一次初始化 dispatch({ type: ActionTypes.INIT }) }
主要是爲 observable
或者 reactive
庫提供的 API
,Reux
內部並無使用這個 API
,暫時不解釋了。
先問個問題:爲何要提供一個 combineReducers
?
我先貼一個正常的 reducer
代碼:
function reducer(state,action){ switch (action.type) { case ACTION_LIST: ... case ACTION_BOOKING: ... } }
當代碼量很小時可能發現不了問題,可是隨着咱們的業務代碼愈來愈多,咱們有了列表頁,詳情頁,填單頁等等,你可能須要處理 state.list.product[0].name
,此時問題就很明顯了:因爲你的 state
獲取到的是全局 state
,你的取數和修改邏輯會很是麻煩。咱們須要一種方案,幫咱們取到局部數據以及拆分 reducers
,這時候 combineReducers
就派上用場了。
源碼核心部分以下:
export default function combineReducers(reducers) { // 各類異常處理和數據清洗 ... return function combination(state = {}, action) { const finalReducers = {}; // 又是各類異常處理,finalReducers 是一個合法的 reducers map ... let hasChanged = false; const nextState = {}; for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; // 獲取前一次reducer const previousStateForKey = state[key]; // 獲取當前reducer const nextStateForKey = reducer(previousStateForKey, action); nextState[key] = nextStateForKey; // 判斷是否改變 hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } // 若是沒改變,返回前一個state,不然返回新的state return hasChanged ? nextState : state; } }
注意這一句,每次都會拿新生成的 state
和前一次的對比,若是引用沒變,就會返回以前的 state
,這也就是爲何值改變後 reducer
要返回一個新對象的緣由。
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
隨着業務量的增大,咱們就能夠利用嵌套的 combineReducers
拼接咱們的數據,可是就筆者的實踐看來,大部分的業務數據都是深嵌套的簡單數據操做,好比我要將 state.booking.people.name
置爲測試姓名,所以咱們這邊有一些別的解決思路,好比使用高階 reducer
,又或者即根據 path
來修改數據,舉個例子:咱們會 dispatch(update('booking.people.name','測試姓名'))
,而後在 reducer
中根據 booking.people.name
這個 path
更改對應的數據。
接受一組函數,會從右至左組合成一個新的函數,好比compose(f1,f2,f3)
就會生成這麼一個函數:(...args) => f1(f2(f3(...args)))
核心就是這麼一句
return funcs.reduce((a, b) => (...args) => a(b(...args)))
拿一個例子簡單解析一下
[f1,f2,f3].reduce((a, b) => (...args) => a(b(...args))) step1: 由於 reduce 沒有默認值,reduce的第一個參數就是 f1,第二個參數是 f2,所以第一個循環返回的就是 (...args)=>f1(f2(...args)),這裏咱們先用compose1 來表明它 step2: 傳入的第一個參數是前一次的返回值 compose1,第二個參數是 f3,能夠獲得這次的返回是 (...args)=>compose1(f3(...args)),即 (...args)=>f1(f2(f3(...args)))
簡單說一下 actionCreator
是什麼
通常咱們會這麼調用 action
dispatch({type:"Action",value:1})
可是爲了保證 action
能夠更好的複用,咱們就會使用 actionCreator
function actionCreatorTest(value){ return { type:"Action", value } } //調用時 dispatch(actionCreatorTest(1))
再進一步,咱們每次調用 actionCreatorTest
時都須要使用 dispatch
,爲了再簡化這一步,就可使用 bindActionCreator
對 actionCreator
作一次封裝,後續就能夠直接調用封裝後的函數,而不用顯示的使用 dispatch
了。
核心代碼就是這麼一段:
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) }
下面的代碼主要是對 actionCreators
作一些操做,若是你傳入的是一個 actionCreator
函數,會直接返回一個包裝事後的函數,若是你傳入的一個包含多個 actionCreator
的對象,會對每一個 actionCreator
都作一個封裝。
export default function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } //類型錯誤 if (typeof actionCreators !== 'object' || actionCreators === null) { throw new Error( ... ) } // 處理多個actionCreators var keys = Object.keys(actionCreators) var boundActionCreators = {} 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 }
想一下這種場景,好比說你要對每次 dispatch(action)
都作一第二天志記錄,方便記錄用戶行爲,又或者你在作某些操做前和操做後須要獲取服務端的數據,這時可能須要對 dispatch
或者 reducer
作一些封裝,redux
應該是想好了這種用戶場景,因而提供了 middleware
的思路。
applyMiddleware
的代碼也很精煉,具體代碼以下:
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) 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 } } }
能夠看到 applyMiddleware
內部先用 createStore
和 reducer
生成了 store
,以後又用 store
生成了一個 middlewareAPI
,這裏注意一下 dispatch: (action) => dispatch(action)
,因爲後續咱們對 dispatch
作了修改,爲了保證全部的 middleware
中能拿到最新的 dispatch
,咱們用了閉包對它進行了一次包裹。
以後咱們執行了
chain = middlewares.map(middleware => middleware(middlewareAPI))
生成了一個 middleware
鏈 [m1,m2,...]
再日後就是 applyMiddleware
的核心,它將多個 middleWare
串聯起來並依次執行
dispatch = compose(...chain)(store.dispatch)
compose
咱們以前有講過,這裏其實就是 dispatch = m1(m2(dispatch))
。
最後,咱們會用新生成的 dispatch
去覆蓋 store
上的 dispatch
可是,在 middleware
內部到底是如何實現的呢?咱們能夠結合 redux-thunk
的代碼一塊兒看看,redux-thunk
主要是爲了執行異步操做,具體的 API
和用法能夠看 github,它的源碼以下:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } // 用next而不是dispatch,保證能夠進入下一個中間件 return next(action); }; }
這裏有三層函數
({ dispatch, getState })=>
這一層對應的就是前面的 middleware(middlewareAPI)
next=>
對應前面 compose
鏈的邏輯,再舉個例子,m1(m2(dispatch))
,這裏 dispatch
是 m2
的 next
,m2(dispatch)
返回的函數是 m1
的 next
,這樣就能夠保證執行 next
時能夠進入下一個中間件
action
這就是用戶輸入的 action
到這裏,整個中間件的邏輯就很清楚了,這裏還有一個點要注意,就是在中間件的內部,dispatch
和 next
是要注意區分的,前面說到了,next
是爲了進入下一個中間件,而因爲以前提到的 middlewareAPI
用到了閉包,若是在這裏執行 dispatch
就會從最一開始的中間件從新再走一遍,若是 middleWare
一直調用 dispatch
就可能致使無限循環。
那麼這裏的 dispatch
的目的是什麼呢?就我看來,其實就是取決與你的中間件的分發思路。好比你在一個異步 action
中又調用了一個異步 action
,此時你就但願再通過一遍 thunk middleware
,所以 thunk
中才會有 action(dispatch, getState, extraArgument)
,將 dispatch
傳回給調用方。
結合這一段時間的學習,讀了第二篇源碼依然會有收穫,好比它利用函數式和 curry
將代碼作到了很是精簡,又好比它的中間件的設計,又能夠聯想到 AOP
和 express
的中間件。
那麼,redux
是如何與 react
結合的?promise
,saga
又是如何實現的?與 thunk
相比有和優劣呢?後面會繼續閱讀源碼,記錄筆記,若是有興趣也能夠 watch
個人 repo 等待後續更新。