理解 Redux 原始碼 (一):來實作 createStore 的 getState、dispatch、subscribe 吧
前言
雖然在先前工作中,較常使用 Context API 以及 useReducer
處理狀態管理,但依然很好奇 Redux 是如何實踐「狀態統一控管」及「單向資料流」的概念,加上看過谷哥在 ModernWeb'21 上分享的 挑戰 40 分鐘實作簡易版 Redux 佐設計模式 於是決定閱讀 Redux 原始碼,並實作簡易的 createStore
函式,主要會聚焦在其中的 getState
、dispatch
以及 subscribe
API。
期許閱讀完本文後,能達成:
- 理解 Redux 是什麼及主要想解決的問題
- 理解
createStore
中的getState
、dispatch
、subscribe
- 理解
subscribe
遇到什麼 bugs,如何藉由currentListners
、nextListners
、ensureCanMutateNextListeners
解決 - 能動手實作基本的
createStore
Redux 是什麼?
在進入實作 Redux createStore
前,先複習 Redux 是什麼及其想解決的問題。
Redux 是一個基於 Flux 流程概念實踐的集中式資料狀態管理的工具,可以使用在 JavaScript 開發的應用程式中,並不限定於 React 或任一框架。
為什麼會需要這個「集中式」的資料狀態管理工具?
主因是前端的複雜性越來越高,且時常同類型的資料會散落在不同的區塊元件中,如果分開管理資料,可能會造成資料狀態不一致的狀況,於是可以透過集中管理資料的方式來解決這個問題。
例如:通常在應用程式中,使用者的資料,如姓名、大頭照、信箱等,會用在不同的區塊元件中,如果沒有集中統一管理資料,可能會造成 A 區塊元件中的信箱資料被更新,但 B 區塊元件中信箱資料卻還是過去的狀態。如果集中管理統一管理資料,亦即使用者的資料來源只有一處,就能解決這個問題。
可從下圖直觀地了解有統一資料來源(store state)、集中式資料狀態庫的好處。
除了「集中式」之外,Redux 還有一個關鍵是基於 Flux 實踐的「單向資料流」更新資料方式,簡言之就是限制更新 store state 的方式,只能透過下圖單向的流程來執行,藉此讓資料的改變更安全、可預期地被控管,概念如下圖:
概要地介紹其中重要的角色:
- Store :
- Redux 的核心,可比喻為一個容器,擁有唯一的資料中心
store state
以及會提供getState
、dispatch
、subscribe
等 API 供外部使用。
- Redux 的核心,可比喻為一個容器,擁有唯一的資料中心
- Dispatcher :
dispatch
會接收action
物件,action
通常包含更新資料的方式action type
及更新資料時所用的值action payload
。- 如果
store state
發生變化,只有一種可能,就是由dispatch
派發action
所觸發的結果。
- Reducer :
- 會接收
dispatch
派發的action
,經由對應action type
的資料更新規則,進行資料更新後,回傳新的store state
。
- 會接收
上面的觀念有個概念即可,記住 Redux 最重要的觀念:
- 會創建單一的中心資料庫
- 修改資料的模式是單向資料流
接著將從這兩個觀念延伸,依據 Redux 原始碼的 pattern,實作負責創建 store 的 createStore
函式。
註:更嚴謹的 Redux 定義,需含 3 個要件 Single source of truth、State is read-only(only change by dispatching)、Changes are made with pure functions,可參考 Redux 文件。
Step 1 : 實作單一資料庫與 getState API
首先,透過函式模組化 (Module Pattern) 的方式,來創建 createStore
並實踐「單一資料庫」的概念:
/*** createStore.js file ***/
// 宣告 createStore 函式
function createStore(preloadedState) {
// 宣告 currentState 作為單一資料來源,並可初始化為 preloadedState
let currentState = preloadedState;
return {};
}
export default createStore;
接著創建 getState
函式,並且讓外部能透過 store.getState
API 接口使用。當呼叫 getState
API 時,會 return currentState
。
/*** createStore.js file ***/
function createStore(preloadedState) {
let currentState = preloadedState;
// 創建取得 currentState 的 getState API
function getState() {
return currentState;
}
// 將內部的 getState function 提供給外部使用
const store = {
getState,
};
return store;
}
export default createStore;
由於 closure 特性,所以在 createStore
中宣告 currentState
變數,不會被 garbage collection 機制回收,因此可以持續存在,提供給外部提取和操作。
Step 2 : 實作更改資料的 dispatch API
實踐 store state
以及 getState
API 後,下一步就來實踐「更新資料」。
先在 createState
內部,創建更新資料的方法,稱之為 dispatch
,並且能傳入 newState
參數,藉此更新 store state
。
/*** createStore.js file ***/
function createStore(preloadedState) {
let currentState = preloadedState;
function getState() {...};
// 創建更新 store state 的 dispatch API
function dispatch(newState) {
// 將 store state 更新為 newState
currentState = newState;
};
const store = {
getState,
dispatch,
};
return store;
};
export default createStore;
看似完成,然而目前更新資料的自由度過高,多人或複雜開發時可能產生非預期問題。
例如:當 store state
中,原本是 number 型別的資料,會進行數字運算,但開發者不小心使用 dispatch('string')
,將資料更新成 string 時,會造成運算上的問題(Bug)。
產生 Bug 的範例如下:
/*** app.js file ***/
import createStore from './createStore.js';
const preloadedState = {
points: 0;
}
// 引入剛剛實作的 createStore 創建 store
const store = createStore(preloadedState);
// 將 state 加 1
store.dispatch({
points: store.getState().points + 1
});
// 將 state 減 1
store.dispatch({
points: store.getState().points - 1
});
// 隨便改,造成後續 Bug,因為將 string 拿去做加減運算爆掉
store.dispatch({
points: 'string'
});
爲了避免上述狀況,會需要有「更新 state
的硬性規則」,分兩個步驟:
- 需要制定更新
store state
的規則,且確保不會有預期外的 side effect。 - 需要修改
store.dispatch
,讓dispatch
按造制訂出的規則更新store state
。
從第 1 點開始實作,開發者能自定義名為 reducer
的 pure function,預先規範更新 store state
的規則,且由於 reducer
為 pure function 不會有 side effect。
接著要將定義好的 reducer
傳入 createStore
,讓 dispatch
時,可以觸發 reducer
,依據制定好的規則更新 store state
,藉此避免剛剛提到的非預期問題。
上述實作如下:
/*** app.js file ***/
import createStore from './createStore.js';
const preloadedState = {
points: 0;
};
// 透過宣告 reducer (pure function),制定修改 state 的規則
// reducer 會傳入 currentState 及修改的 action 是什麼
function reducer(state, action) {
switch (action.type) {
// 當 action.type 是 INCREMENT 時,執行下面更新資料的邏輯
case 'INCREMENT':
return {
...state,
points: state.points + 1;
}
// 當 action.type 是 DECREMENT 時,執行下面更新資料的邏輯
case 'DECREMENT':
return {
...state,
points: state.points - 1;
}
default:
return state;
};
};
// 將制定好的 reducer 傳入 createStore 中,提供給 dispatch 使用
const store = createStore(reducer, preloadedState);
// 僅能透過定義好的 action.type "INCREMENT" 修改 state
store.dispatch({
type: 'INCREMENT',
});
// 僅能透過制定好的 action.type "DECREMENT" 修改 state
store.dispatch({
type: 'DECREMENT'
});
接續實作第 2 點,優化 createStore
中的 dispatch
,使其能依據 reducer
規則修改 store state
。
具體而言,就是讓 dispatch
能接收 action
,並透過執行 reducer
函式,更新 store state
。
/*** createStore.js file ***/
// 新增 reducer 參數,由外部定義後傳入
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
let currentReducer = reducer;
function getState() {...};
// dispatch 可傳入 action object
// 通常 action object 會有 action.type 與 action.payload
function dispatch(action) {
// 透過 reducer 中定義好的規則,更新 store currentState
currentState = currentReducer(currentState, action)
};
const store = {
getState,
dispatch,
};
return store;
};
export default createStore;
至此,就完成更新 store state
的 dispatch
。複習一下,它做了兩件事情:
- 可以接收
action
參數 - 將
action
傳遞給reducer
,藉此更新store state
Step 3 : 透過 isDispatching 優化 getState 以及 dispatch
到此為止可思考一件事:當 reducer
更新 state
時,如果再次觸發 getState
或 dispatch
會不會造成什麼問題?
換句話說更具體地說:外部使用方能否在自定義的 reducer
內,使用 store.getState
與 store.dispatch
?
- getState : 不必要,因為
reducer
本身首個參數已接收到目前state
,可直接取用,無須呼叫store.getState
。 - dispatch : 不必要,因為預期一個
action
同時只會更動一次state
。而且需要避免reducer
執行時action
時,又再度觸發store.dispatch
執行同個action
的狀況,這會導致無限遞迴的 Bug。
撇除上面兩項說明,還有個重要思維是 reducer 的本質是專注在接收 action ,並且根據 action type,執行定義好的邏輯去更新 state,更新後回傳。是純 pure function,任何多餘的 side effect 都該避免。
可以透過新增 isDispatching
flag,來達成在 reducer
執行中,禁止呼叫 getState
與 dispatch
,流程如下:
- 在
dispatch
中,reducer
要執行前,將isDispatching = true
- 等到
reducer
執行完畢後,把isDispatching = false
- 如果在
getState
與dispatch
中,遇到isDispatching = true
就拋出 error message
藉此達成在 reducer
中,無法使用 getState
與 dispatch
的目標。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
let currentReducer = reducer;
// 新增 isDispatching flag 去判斷是否正在執行 reducer
// 在 dispatch 函式執行 reducer 前會轉為 true
let isDispatching = false;
function getState() {
// 在 reducer 內不能使用 store.getState()
// => isDispatching = true 時要噴錯誤訊息
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. ' +
'Get the state from the top reducer instead of reading it from the store.'
);
}
return currentState;
}
function dispatch(action) {
// 在 reducer 內不能使用 store.dispatch()
// => isDispatching = true 時要噴錯誤訊息
if (isDispatching) {
throw new Error('Reducers may not dispatch actions when isDispatching.');
}
try {
// 將 isDispatching 轉為 true 並執行 reducer
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
// reducer 執行結束時,將 isDispatching 轉為 false
isDispatching = false;
}
}
const store = {
getState,
dispatch,
};
return store;
}
export default createStore;
Step 4 : 實作訂閱機制的 subscribe API
假如有個新需求,希望每當 store state
中的 points 被更新時,要自動 console.log
points,在 app.js 中期望的使用方式如下:
/*** app.js file ***/
import createStore from './createStore.js';
......
const store = createStore(reducer, preloadedState);
// 每當 store state 改變時,就執行 callback function,印出 points
// store 會透過 subscribe 接收這個 callback function
store.subscribe(() => {
console.log(store.getState().points)
});
......
subscribe
,需要實踐兩項重要的概念:
- 可以傳入 callback function 作為訂閱函式
- 當
store state
改變後,訂閱的 callback function 會被執行
透過下面程式碼,能實作上述概念:
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
let currentReducer = reducer;
let isDispatching = false;
// 定義 listener,在 subscribe 中被更新 ; 在 dispatch 中被執行
let listener = null
function getState() {...};
function dispatch(action) {
...
// 當 state 經由 reducer 更新後,
// 如果 listener 有已訂閱項目,就會執行 listener
if(listener) {
listener();
}
};
// store.subscribe 可傳入 callback function,命名為 listenerCb
function subscribe(listenerCb) {
// 在 reducer 執行時,不能新增訂閱
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."
);
}
// 訂閱 listenerCb
listener = listenerCb;
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
接續以需求面,思考兩個問題:
- 是否有需要取消訂閱的時候?會,所以需要有 unsubscribe 的方法。
- 是否有需要訂閱多個事件(callback)的時候?會,所以需要 listeners array 而非 listener 而已。
先處理第 1 點「取消訂閱」的需求,讓 subscribe
會 return unsubscribe
,如此一來就能透過執行 unsubscribe()
來取消訂閱。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
function subscribe(listenerCb) {
if (isDispatching) {...}
// 新增 isSubscribe flag 判斷 unsubscribe 是否要被執行
let isSubscribed = true;
listener = listenerCb;
// 新增 unsubscribe 作為取消訂閱時使用
return unsubscribe () {
if (!isSubscribed) {
return;
}
if (isDispatching) {
throw new Error(
"You may not unsubscribe from a store listener while the reducer is executing."
);
}
// unsubscribe 被執行時,listener 改回 null,藉此移除訂閱者
listener = null;
isSubscribed = false;
}
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
接著處理第 2 個需求「訂閱多個 callback 事件」:
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
// 將 listener 改為 listeners = []
let listeners = [];
function getState() {...};
function dispatch(action) {
...
// 在 state 更新後,執行所有訂閱的 listener 事件
for(let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
function subscribe(listener) {
...
// 新訂閱的 listener,會被加入 listeners
listeners.push(listener);
return unsubscribe () {
...
// 移除訂閱的事件,會被抽離 listeners
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
isSubscribed = false;
}
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
到此就完成 subscribe
/unsubscribe
基本功能了。
Step 5 : 修復多層 subscribe / unsubscribe 的問題
目前實作的 Redux 在開發端執行多層 subscribe
/unsubscribe
時會有問題,如下:
/*** app.js ***/
const store = createStore(reducer, preloadedState);
const unsubscribe1 = store.subscribe(() => {...});
const unsubscribe2 = store.subscribe(() => {
// 在 subscribe callback 中執行 unsubscribe 會有問題
unsubscribe1();
// 在 subscribe callback 中執行另一個 subscribe 也有問題
const unsubscribe3 = store.subscribe(() => {...});
});
為什麼上面這樣會有問題?
因為在針對 listeners array 進行 for loop 時,更改到 listeners 的長度,所以會造成非預期的執行狀況。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
function dispatch(action) {
...
for(let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
// 假設在這個 listener 執行時,改變 listeners 的長度
// 就可能導致有項目被跳過沒有被執行,或是非預期地多被執行
listener();
};
};
...
};
export default createStore;
處理此問題,可以透過確保正在執行的 listeners 不會被 subscribe / unsubscribe 影響。
透過創建 currentListeners
與 nextListeners
達成這個目的:
- currentListeners : stable, 正在被 for 迴圈執行的 listeners
- nextListeners : unstable, 會被
subscribe
及unsubscribe
改變的 listeners
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
// 將 listeners 改為 currentListeners 及 nextListeners
let currentListeners = [];
let nextListeners = currentListeners;
function getState() {...};
function dispatch(action) {
...
// 在執行 currentListeners 前,先將 currentListeners 更新成最新的 listeners
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
function subscribe(listener) {
...
// subscribe 時,改為對 nextListeners 進行操作
nextListeners.push(listener);
return unsubscribe () {
...
// unsubscribe 時,改為對 nextListeners 進行操作
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
isSubscribed = false;
}
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
特別注意的是 array 為 object data 複製時是複製 reference 而非 value,所以新增 ensureCanMutateNextListeners
處理這個問題。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
let currentListeners = [];
let nextListeners = currentListeners;
function getState() {...};
// 利用淺拷貝,確保 nextListeners 與 currentListeners 不會指向同一個資料
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
function dispatch(action) {
...
// 在執行 currentListeners 前,先將 currentListeners 更新成最新的 listeners
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
};
function subscribe(listener) {
...
// 先執行 ensureCanMutateNextListeners,
// 確保對 nextListeners 操作,絕不會改到 currentListeners
ensureCanMutateNextListeners();
nextListeners.push(listener);
return unsubscribe () {
...
// 先執行 ensureCanMutateNextListeners,
// 確保對 nextListeners 操作,絕不會改到 currentListeners
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
isSubscribed = false;
}
};
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
經過以上的處理,才算完整地實作 subscribe
/unsubscribe
。
Step 6 : 添加初始化的 dispatch
最後一步,來到最後一步了!
添加初始化用 dispatch
,讓最初的 state
返回 reducer
中所設定的 initialState
。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
...
// 給予一串隨機的字串
const randomString = () =>
Math.random().toString(36).substring(7).split("").join(".");
// 初始化 state 的 dispatch
// 利用 randomString 避免與使用者自定義的 INIT action type 衝突
dispatch({
type: `INIT${randomString()}`,
});
const store = {
getState,
dispatch,
subscribe,
};
return store;
};
export default createStore;
至此就完成 Redux 核心的 createStore
!
回顧整個 createStore 程式碼
整段程式碼如下,附上註解解釋,如需無註解的版本,可以點此到 Github 上觀看。
/*** createStore.js file ***/
function createStore(reducer, preloadedState) {
// currentState 就是核心的 store state,初始的 preloadedState 由外部傳入
let currentState = preloadedState;
// currentReducer 就是更新 state 會使用的 reducer,由外部傳入
let currentReducer = reducer;
// currentListeners 以及 nextListeners 是為了 subscribe 功能而設計
let currentListeners = [];
let nextListeners = currentListeners;
// isDispatching flag 是為了避免 reducer 中使用 getState、dispatch、subscribe 而設計
let isDispatching = false;
// ensureCanMutateNextListeners 利用淺拷貝,確保 nextListeners 以及 currentListeners 第一層指向不同來源
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
// 外部可透過 store.getState 取得 store state
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. ' +
'Get the state from the top reducer instead of reading it from the store.'
);
}
return currentState;
}
// 外部透過 store.dispatch(action) 改變 store state
function dispatch(action) {
if (isDispatching) {
throw new Error('Reducers may not dispatch actions when isDispatching.');
}
try {
isDispatching = true;
// 透過 currentReducer 的執行,返回更新後的 store state
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
// store state 更新後,先更新 currentListeners,接著觸發所有訂閱的 listener
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
// 外部透過 store.subscribe(listener) 訂閱、 unsubscribe(listener) 取消訂閱 listener
function subscribe(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.'
);
}
let isSubscribed = true;
// 將新的 listener 添加到 nextListeners 前,先確保 currentListeners 與 nextListeners 不同
ensureCanMutateNextListeners();
nextListeners.push(listener);
return function unsubscribe(listener) {
if (!isSubscribed) {
return;
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. '
);
}
// 將 listener 從 nextListeners 移除前,先確保 currentListeners 與 nextListeners 不同
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
isSubscribed = false;
};
}
const randomString = () => Math.random().toString(36).substring(7).split('').join('.');
// initialize,透過 randomString() 避免與外部使用者定義的 INIT action type 撞名
dispatch({
type: `INIT${randomString()}`,
});
const store = {
getState,
dispatch,
subscribe,
};
return store;
}
export default createStore;
同時附上使用 createStore
的範例 app.js
檔案:
/*** app.js file ***/
import { createStore } from './createStore.js';
// 自定義 reducer
const reducer = (state, action) => {
switch (action.type) {
// 如果接收到 PLUS_POINTS 的 action.type,就增加 points
case 'PLUS_POINTS':
return {
points: state.points + action.payload,
};
// 如果接收到 MINUS_POINTS 的 action.type,就減少 points
case 'MINUS_POINTS':
return {
points: state.points - action.payload,
};
default:
return state;
}
};
// 將自定義的 reducer 傳入 createStore 中,藉此創建 store
// store 會提供 getState、dispatch、subscribe API
const preloadedState = {
points: 0,
};
const store = createStore(reducer, preloadedState);
// 當 plus 按鈕被點擊時,就觸發 callback,增加 100 points
document.getElementById('plus-points-btn').addEventListener('click', () => {
// 透過 dispatch { type: 'PLUS_POINTS', payload: 100 }
// 將 store state 中的 points 增加 100
store.dispatch({
type: 'PLUS_POINTS',
payload: 100,
});
});
// 當 minus 按鈕被點擊時,就觸發 callback,減少 100 points
document.getElementById('minus-points-btn').addEventListener('click', () => {
// 透過 dispatch { type: 'MINUS_POINTS', payload: 100 }
// 將 store state 中的 points 減少 100
store.dispatch({
type: 'MINUS_POINTS',
payload: 100,
});
});
// 透過 subscribe 訂閱機制,當資料被更新時,就會執行傳入的 callback
store.subscribe(() => {
// 透過 getState 取出最新的 points 並 render 到畫面上
const points = store.getState().points;
document.getElementById('display-points-automatically').textContent = points;
});
回顧閱讀文章後要達成的目標
來回文章最初幾個希望閱讀後,能達成的項目:
1. 理解 Redux 是什麼,以及主要想解決的問題
Redux 是一個基於 Flux 流程概念實踐的「集中式」資料狀態管理的工具,最主要的目的是統一管理資料,避免資料狀態不一致的問題,且也利用「單向資料流」的方式控管資料狀態,讓資料變動更可預期與維護。
2. 理解並實作 createStore 中的 getState、dispatch、subscribe
createStore
的核心在於單一控管的 sore state
,且提供下列三個 API :
- getState : 取得目前的
store state
。 - dispatch : 透過傳入
action
(含 type and payload) 更新store state
。 - subscribe : 透過傳入 callback,就能訂閱 callback,在
sotore state
更新後會執行 callback。
3. 理解 subscribe 會遇到什麼 bugs,如何藉由 currentListners、nextListners、ensureCanMutateNextListeners 解決
如果不特別處理,在 subscribe
傳入的 listner callback 中執行另一個 subscribe
或 unsubscribe
可能遇到非預期 Bugs。
解決方案的關鍵是:
- currentListners : 創建
currentListners
,stable,在state
變動後,會真的執行其中每個listner
。 - nextListners : 創建
nextListners
,unstable,當subscribe
/unsubscribe
時,會在nextListners
中新增或移除listner
。 - ensureCanMutateNextListeners : 因為
listners
是 array,為確保currentListners
與nextListners
不同,因此在nextListners
操作前,會先執行ensureCanMutateNextListeners
。
4. 能動手實作 basic createStore function
非常推薦可以自己實作,印象會更深刻!如果卡住,再隨時回顧本文或 Redux 的原始碼。
雖然此篇文章尚未做出完整的 createStore
,像是沒實作 enhancer
相關功能,但透過實作 getState
、dispatch
、subscribe
,已能理解核心的 Redux 運作,也知道它是如何透過 closure、listeners 等模式,去封裝並實踐集中式資料管理以及監聽資料變化等概念,非常有趣。
如果對 enhancer
或 middlewares
機制有興趣,歡迎閱讀下篇文章:理解 Redux 原始碼 (二):來實作 middlewares、applyMiddleware 以及 createStore enhancer 吧。