理解 Redux 原始碼(二)- createStore

  這邊要介紹的 createStore 是整個 Redux 的核心,包含了你所需要的全局狀態以及大部分的 API,在理解時可以打開 github 程式碼跟著閱讀,我會在本篇以及往後的說明把 ts 轉換成 js 程式碼以方便閱讀。

碎碎念

  在正式進入前想提一下,Redux 在原始碼中加了 @deprecated 註解,呼籲大家不要使用 createStore 而是建議使用 Redux-toolkit 所提供的 configureStore 方法,但這並不是說 createStore 就完全沒用了哦!其實 configureStore 底層也是使用了 createStore 方法,只是把整個架構包裝得更好,所以還是很值得去理解一下 createStore 的!(跑不掉的)

  再補充一下,在說明時我不一定會按照程式碼編寫的順序進行說明,而是會以我認為比較好理解整個脈絡的順序下去說明,但最後還是會涵蓋到平常較常用的方法。

  接著就來看一下程式碼吧!

正文

一、參數型別判斷

createStore(reducer, preloadedState, enhancer) {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
)
}

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. Instead, received: '${kindOf(
enhancer
)}'`
)
}

return enhancer(createStore)(reducer, preloadedState)
}

//...
}

  首先能看到 createStore 接受 reducer, preloadedState, enhancer 三個參數,且一開始針對這三個參數是否傳入以及傳入的型別去判斷

  1. 傳入參數的型別錯誤會拋 error
  2. 只傳入兩個參數會判斷第二個參數的型別從而得知 user 想傳入的是 preloadedState 還是 enhancer
  3. 如果有傳入 enhancer 則返回呼叫後再呼叫一次的結果

  針對 enhancer 的內容後面會再說明,這裡先略過並不影響閱讀。


二、變數

  接著定義了一些變數,而這些變數充分利用閉包的特性,可以在呼叫從 store 暴露的各種方法時被利用,共同維持整個 store 功能。

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let listenerIdCounter = 0
let isDispatching = false

  大概說明一下每個變數的作用

currentReducer

  我們知道 reducer 是一個根據 action type 來決定要返回什麼樣 state 格式的方法,為了維持彈性,store 也暴露出 replaceReducer 這個方法讓使用者來替換 reducer,而 currentReducer 就是用來記錄目前的 reducer

currentState

  對,就是目前的狀態。

currentListeners

  記錄目前由 subscribe 訂閱的所有 callbacks,所有 callbacks 會於每次呼叫 dispatch 時被觸發。

nextListeners

  一樣用來記錄目前訂閱的 callbacks,至於為什麼需要這個變數後面會提到。

listenerIdCounter

  在 (un)subscribe 時用來記錄要訂閱或解訂閱哪一個 callback。

isDispatching

  顧名思義用來記錄目前是否正在呼叫 dispatch,redux 設計上在某些場景需要這個 flag 來阻止不想要的行為,後面會提到。


三、dispatch

  接著看一下 dispatch function 做了些什麼

參數型別判斷

if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(
action
)}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
)
}

if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
)
}

if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}

  首先 dispatch 會針對傳入的 action 做型別判斷,這邊用到的 isPlainObject 這個 redux 自行寫的 util function,主要是確保型別真的是一個狹義的 物件 ,而不是使用 typeof 去判斷型別,避免像是 array 或 null 等會被誤判成物件

  接著判斷如果 action 沒有給予 type 屬性,或是 isDispatch 為 true 時也會噴錯

呼叫 reducer

try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}

  這裡是整個 dispatch 方法最重要的地方,主要執行了兩件事:

  1. action 以及此刻的 state 傳給 currentReducer 去返回新的狀態
  2. 呼叫 currentReducer 前將 isDispatching 改為 true,呼叫完改為 false,這裡也是唯一一個會改變 isDispatching 參數的地方,換句話說,createStore 內有用到 isDispatching 判斷的地方像是 (un)subscribe, dispatch, getState 就是在防止這些方法在 reducer 內被呼叫

執行 listeners,返回 action

const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

return action

  這裡就比較好理解,最後返回 action 根據原始碼註解來看純粹是為了給個方便,至於為什麼呼叫 currentListeners 前還要將變數指向 nextListeners 就得看一下 subscribe 這個方法了。


四、subscribe

Observer Pattern

  在談論 subscribe 方法之前建議先了解其實這個方法利用的是設計模式中的 Observer Pattern,而這個模式中會有一個觀察主體以及與他相依的 callback 陣列,主體可以在陣列中加入(subscribe)新的 callback 或者移除(unsubscribe)已知的 callback,並且在某個特定事件發生時呼叫目前所有的 callback。

  而 Redux 的例子中,createStore 所返回的物件就是觀察的主體,而 currentListeners 或是 nextListeners 就是用來存放 callback 的變數,dispatch 方法就是觸發所有 callback 的事件。

  而不了解 Observer Pattern 其實也不會看不懂 subscribe 方法,但建議有個脈絡會比較知道前因後果,這邊僅是大概介紹,如果想進一步了解可以看 pattern.dev 的解說,我認為寫得很好!

  接著繼續往下看。

型別檢查與呼叫限制

function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error(
`Expected the listener to be a function. Instead, received: '${kindOf(
listener
)}'`
)
}

if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}

//...
}

  只接受一個 callback(listener),必須是 function 型別的參數,並且無法在 reducer 內呼叫。

訂閱與取消訂閱

let isSubscribed = true

ensureCanMutateNextListeners()
const listenerId = listenerIdCounter++
nextListeners.set(listenerId, listener)

return function unsubscribe() {
if (!isSubscribed) {
return
}

if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}

isSubscribed = false

ensureCanMutateNextListeners()
nextListeners.delete(listenerId)
currentListeners = null
}

  這段在做的事情是將 callback 加入 nextListeners 並返回對應的 unsubscribe 方法,並利用 isSubscribed 做性能優化,同樣的 unsubscribe 方法被呼叫第二次就直接 return 掉。

  看到這一段會發現,在每次訂閱或者取消訂閱時其實是在 nextListeners 變數處理的,而這也是為什麼 dispatch 在呼叫 listeners 方法時需要先將 currentListeners 更新為 nextListeners

  這邊細看一下 ensureCanMutateNextListeners 這個方法

/**
* This makes a shallow copy of currentListeners so we can use
* nextListeners as a temporary list while dispatching.
*
* This prevents any bugs around consumers calling
* subscribe/unsubscribe in the middle of a dispatch.
*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = new Map()
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener)
})
}
}

  這個方法做的是檢查 nextListenerscurrentListeners 是否嚴格相等,如果是的話就淺拷貝一份並賦值給 nextListeners ,而 ensureCanMutateNextListeners 會在每次將 callback 加入或移除 nextListeners 前執行,而不這麼的話會發生什麼事呢?

  假如 listeners 目前有兩個 callback,而在第一個 callback 中呼叫了 unsubscribe 方法將第二個 callback 從 listeners 移除了,那麼第二個 callback 就永遠不會被呼叫到。

  所以就如同註解上說的,這麼做的好處是可以在 dispatch 時保留 listeners 的 snapshot 而在呼叫 listeners 時不被後續的 subscribe / unsubscribe 影響。

五、getState

function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}

return currentState
}

  這應該就不用我多說了XD

六、replaceReducer

function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
nextReducer
)}`
)
}

currentReducer = nextReducer

// This action has a similiar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE })
}

  這個方法不難理解,更換 currentReducer 後執行一次 dispatch 進行初始化,至於 ActionTypes.REPLACE 只是 Redux 隨機產生的亂數字串,目的是確保 currentState 維持在最新的狀態

  這個方法是 redux 曝露出來讓 user 可以動態更換 reducer 的方法,好處是不用每次想更改 reducer 時都要重新建立一個 store,但事實上大部分專案不太會用到這個方法。


返回方法

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)

const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
return store

在返回所有方法之前先執行一次 dispatch,用意與 replaceReducer 內執行的 dispatch 相同,為了初始化狀態。


小結

這篇探討了 createStore 是如何以閉包內的變數與傳入的變數交互作用形成平常我們使用的 store 方法,但還有一些部分沒有講到像是 enhancer 的作用,以及 reducer 的細節,下一篇會細看 combineReducer 的原始碼去了解為什麼 Redux 強調 reducer 內改變狀態必須是 immutable,以及 reducer 是如何比較前後 state 是否相等的。



參考文獻

Redux 官方文件