同步自我的 部落格
很久之前就看過一遍 Redux
相關技術棧的原始碼,最近在看書的時候發現有些細節已經忘了,而且發現當時的理解有些偏差,打算寫幾篇學習筆記。這是第一篇,主要記錄一下我對 Redux
、redux-thunk
原始碼的理解。我會講一下大體的架構,和一些核心部分的程式碼解釋,更具體的程式碼解釋可以去看我的 repo,後續會繼續更新 react-redux
,以及一些別的 redux
中介軟體的程式碼和學習筆記。
注意:本文不是單純的講 API
,如果不瞭解的可以先看一下文件,或者 google
一下 Redux
相關的基礎內容。
整體架構
在我看來,Redux 核心理念很簡單
store
負責儲存資料- 使用者觸發
action
reducer
監聽action
變化,更新資料,生成新的store
程式碼量也不大,原始碼結構很簡單:
.src
|- utils
|- applyMiddleware.js
|- bindActionCreators.js
|- combineReducers.js
|- compose.js
|- createStore.js
|- index.js複製程式碼
其中 utils
只包含一個 warning
相關的函式,這裡就不說了,具體講講別的幾個函式
index.js
這是入口函式,主要是為了暴露 Redux
的 API
這裡有這麼一段程式碼,主要是為了校驗非生產環境下是否使用的是未壓縮的程式碼,壓縮之後,因為函式名會變化,isCrushed.name
就不等於 isCrushed
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
warning(...)
)}複製程式碼
createStore
這個函式是 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
這裡的 ActionTypes
主要是宣告瞭一個預設的 action
,用於 reducer
的初始化。
ensureCanMutateNextListeners
它的目的主要是儲存一份快照,下面我們就講講 subscribe
,以及為什麼需要這個快照
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
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
。
getState
獲取當前的 state
,程式碼很簡單,就不貼了。
replaceReducer
更換當前的 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
主要是為 observable
或者 reactive
庫提供的 API
,Reux
內部並沒有使用這個 API
,暫時不解釋了。
combineReducers
先問個問題:為什麼要提供一個 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
接受一組函式,會從右至左組合成一個新的函式,比如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)))複製程式碼
bindActionCreator
簡單說一下 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
}複製程式碼
applyMiddleware
想一下這種場景,比如說你要對每次 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 等待後續更新。