理解 Redux 原始碼(二)- createStore
這邊要介紹的 createStore
是整個 Redux 的核心,包含了你所需要的全局狀態以及大部分的 API,在理解時可以打開 github 程式碼跟著閱讀,我會在本篇以及往後的說明把 ts 轉換成 js 程式碼以方便閱讀。
碎碎念
在正式進入前想提一下,Redux 在原始碼中加了 @deprecated
註解,呼籲大家不要使用 createStore
而是建議使用 Redux-toolkit
所提供的 configureStore
方法,但這並不是說 createStore
就完全沒用了哦!其實 configureStore
底層也是使用了 createStore
方法,只是把整個架構包裝得更好,所以還是很值得去理解一下 createStore
的!(跑不掉的)
再補充一下,在說明時我不一定會按照程式碼編寫的順序進行說明,而是會以我認為比較好理解整個脈絡的順序下去說明,但最後還是會涵蓋到平常較常用的方法。
接著就來看一下程式碼吧!
正文
一、參數型別判斷
createStore(reducer, preloadedState, enhancer) { |
首先能看到 createStore 接受 reducer
, preloadedState
, enhancer
三個參數,且一開始針對這三個參數是否傳入以及傳入的型別去判斷
- 傳入參數的型別錯誤會拋 error
- 只傳入兩個參數會判斷第二個參數的型別從而得知 user 想傳入的是
preloadedState
還是enhancer
- 如果有傳入
enhancer
則返回呼叫後再呼叫一次的結果
針對 enhancer 的內容後面會再說明,這裡先略過並不影響閱讀。
二、變數
接著定義了一些變數,而這些變數充分利用閉包的特性,可以在呼叫從 store 暴露的各種方法時被利用,共同維持整個 store 功能。
let currentReducer = reducer |
大概說明一下每個變數的作用
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)) { |
首先 dispatch
會針對傳入的 action
做型別判斷,這邊用到的 isPlainObject
這個 redux 自行寫的 util function,主要是確保型別真的是一個狹義的 物件
,而不是使用 typeof 去判斷型別,避免像是 array 或 null 等會被誤判成物件
接著判斷如果 action 沒有給予 type 屬性,或是 isDispatch 為 true 時也會噴錯
呼叫 reducer
try { |
這裡是整個 dispatch
方法最重要的地方,主要執行了兩件事:
- 將
action
以及此刻的 state 傳給currentReducer
去返回新的狀態 - 呼叫
currentReducer
前將isDispatching
改為 true,呼叫完改為 false,這裡也是唯一
一個會改變isDispatching
參數的地方,換句話說,createStore 內有用到 isDispatching 判斷的地方像是 (un)subscribe, dispatch, getState就是在防止這些方法在 reducer 內被呼叫
執行 listeners,返回 action
const listeners = (currentListeners = nextListeners) |
這裡就比較好理解,最後返回 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) { |
只接受一個 callback(listener),必須是 function 型別的參數,並且無法在 reducer 內呼叫。
訂閱與取消訂閱
let isSubscribed = true |
這段在做的事情是將 callback 加入 nextListeners
並返回對應的 unsubscribe 方法,並利用 isSubscribed
做性能優化,同樣的 unsubscribe 方法被呼叫第二次就直接 return 掉。
看到這一段會發現,在每次訂閱或者取消訂閱時其實是在 nextListeners
變數處理的,而這也是為什麼 dispatch
在呼叫 listeners 方法時需要先將 currentListeners
更新為 nextListeners
。
這邊細看一下 ensureCanMutateNextListeners
這個方法
/** |
這個方法做的是檢查 nextListeners
與 currentListeners
是否嚴格相等,如果是的話就淺拷貝一份並賦值給 nextListeners
,而 ensureCanMutateNextListeners
會在每次將 callback 加入或移除 nextListeners 前執行,而不這麼的話會發生什麼事呢?
假如 listeners 目前有兩個 callback,而在第一個 callback 中呼叫了 unsubscribe 方法將第二個 callback 從 listeners 移除了,那麼第二個 callback 就永遠不會被呼叫到。
所以就如同註解上說的,這麼做的好處是可以在 dispatch 時保留 listeners 的 snapshot 而在呼叫 listeners 時不被後續的 subscribe / unsubscribe 影響。
五、getState
function getState() { |
這應該就不用我多說了XD
六、replaceReducer
function replaceReducer(nextReducer) { |
這個方法不難理解,更換 currentReducer 後執行一次 dispatch 進行初始化,至於 ActionTypes.REPLACE
只是 Redux 隨機產生的亂數字串,目的是確保 currentState 維持在最新的狀態
這個方法是 redux 曝露出來讓 user 可以動態更換 reducer 的方法,好處是不用每次想更改 reducer 時都要重新建立一個 store,但事實上大部分專案不太會用到這個方法。
返回方法
// When a store is created, an "INIT" action is dispatched so that every |
在返回所有方法之前先執行一次 dispatch,用意與 replaceReducer
內執行的 dispatch 相同,為了初始化狀態。
小結
這篇探討了 createStore
是如何以閉包內的變數與傳入的變數交互作用形成平常我們使用的 store 方法,但還有一些部分沒有講到像是 enhancer 的作用,以及 reducer 的細節,下一篇會細看 combineReducer
的原始碼去了解為什麼 Redux 強調 reducer 內改變狀態必須是 immutable,以及 reducer 是如何比較前後 state 是否相等的。
參考文獻