Redux但是一個大名鼎鼎的庫,不少地方都在用,我也用了幾年了,今天這篇文章就是本身來實現一個Redux,以便於深刻理解他的原理。咱們仍是老套路,從基本的用法入手,而後本身實現一個Redux來替代源碼的NPM包,可是功能保持不變。本文只會實現Redux的核心庫,跟其餘庫的配合使用,好比React-Redux準備後面單獨寫一篇文章來說。有時候咱們過於關注使用,只記住了各類使用方式,反而忽略了他們的核心原理,可是若是咱們想真正的提升技術,最好仍是一個一個搞清楚,好比Redux和React-Redux看起來很像,可是他們的核心理念和關注點是不一樣的,Redux其實只是一個單純狀態管理庫,沒有任何界面相關的東西,React-Redux關注的是怎麼將Redux跟React結合起來,用到了一些React的API。javascript
本文所有代碼已經上傳到GitHub,你們能夠拿下來玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux前端
基本概念
Redux的概念有不少文章都講過,想必你們都看過不少了,我這裏再也不展開,只是簡單提一下。Redux基本概念主要有如下幾個:java
Store
人如其名,Store就是一個倉庫,它存儲了全部的狀態(State),還提供了一些操做他的API,咱們後續的操做其實都是在操做這個倉庫。假如咱們的倉庫是用來放牛奶的,初始狀況下,咱們的倉庫裏面一箱牛奶都沒有,那Store的狀態(State)就是:react
{ milk: 0 }
Actions
一個Action就是一個動做,這個動做的目的是更改Store中的某個狀態,Store仍是上面的那個倉庫,如今我想往倉庫放一箱牛奶,那"我想往倉庫放一箱牛奶"就是一個Action,代碼就是這樣:git
{ type: "PUT_MILK", count: 1 }
Reducers
前面"我想往倉庫放一箱牛奶"只是想了,還沒操做,具體操做要靠Reducer,Reducer就是根據接收的Action來改變Store中的狀態,好比我接收了一個PUT_MILK
,同時數量count
是1,那放進去的結果就是milk
增長了1,從0變成了1,代碼就是這樣:github
const initState = { milk: 0 } function reducer(state = initState, action) { switch (action.type) { case 'PUT_MILK': return {...state, milk: state.milk + action.count} default: return state } }
能夠看到Redux自己就是一個單純的狀態機,Store存放了全部的狀態,Action是一個改變狀態的通知,Reducer接收到通知就更改Store中對應的狀態。json
簡單例子
下面咱們來看一個簡單的例子,包含了前面提到的Store,Action和Reducer這幾個概念:redux
import { createStore } from 'redux'; const initState = { milk: 0 }; function reducer(state = initState, action) { switch (action.type) { case 'PUT_MILK': return {...state, milk: state.milk + action.count}; case 'TAKE_MILK': return {...state, milk: state.milk - action.count}; default: return state; } } let store = createStore(reducer); // subscribe其實就是訂閱store的變化,一旦store發生了變化,傳入的回調函數就會被調用 // 若是是結合頁面更新,更新的操做就是在這裏執行 store.subscribe(() => console.log(store.getState())); // 將action發出去要用dispatch store.dispatch({ type: 'PUT_MILK' }); // milk: 1 store.dispatch({ type: 'PUT_MILK' }); // milk: 2 store.dispatch({ type: 'TAKE_MILK' }); // milk: 1
本身實現
前面咱們那個例子雖然短小,可是已經包含了Redux的核心功能了,因此咱們手寫的第一個目標就是替換這個例子中的Redux。要替換這個Redux,咱們得先知道他裏面都有什麼東西,仔細一看,咱們好像只用到了他的一個API:數據結構
createStore
:這個API接受reducer
方法做爲參數,返回一個store
,主要功能都在這個store
上。app
看看store
上咱們都用到了啥:
store.subscribe
: 訂閱state
的變化,當state
變化的時候執行回調,能夠有多個subscribe
,裏面的回調會依次執行。
store.dispatch
: 發出action
的方法,每次dispatch
action
都會執行reducer
生成新的state
,而後執行subscribe
註冊的回調。
store.getState
:一個簡單的方法,返回當前的state
。
看到subscribe
註冊回調,dispatch
觸發回調,想到了什麼,這不就是發佈訂閱模式嗎?我以前有一篇文章詳細講過發佈訂閱模式了,這裏直接仿寫一個。
function createStore() { let state; // state記錄全部狀態 let listeners = []; // 保存全部註冊的回調 function subscribe(callback) { listeners.push(callback); // subscribe就是將回調保存下來 } // dispatch就是將全部的回調拿出來依次執行就行 function dispatch() { for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } } // getState直接返回state function getState() { return state; } // store包裝一下前面的方法直接返回 const store = { subscribe, dispatch, getState } return store; }
上述代碼是否是很簡單嘛,Redux核心也是一個發佈訂閱模式,就是這麼簡單!等等,好像漏了啥,reducer
呢?reducer
的做用是在發佈事件的時候改變state
,因此咱們的dispatch
在執行回調前應該先執行reducer
,用reducer
的返回值從新給state
賦值,dispatch
改寫以下:
function dispatch(action) { state = reducer(state, action); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } }
到這裏,前面例子用到的全部API咱們都本身實現了,咱們用本身的Redux來替換下官方的Redux試試:
// import { createStore } from 'redux'; import { createStore } from './myRedux';
能夠看到輸出結果是同樣的,說明咱們本身寫的Redux沒有問題:
瞭解了Redux的核心原理,咱們再去看他的源碼應該就沒有問題了,createStore的源碼傳送門。
最後咱們再來梳理下Redux的核心流程,注意單純的Redux只是個狀態機,是沒有View
層的哦。
除了這個核心邏輯外,Redux裏面還有些API也頗有意思,咱們也來手寫下。
手寫combineReducers
combineReducers
也是使用很是普遍的API,當咱們應用愈來愈複雜,若是將全部邏輯都寫在一個reducer
裏面,最終這個文件可能會有成千上萬行,因此Redux提供了combineReducers
,可讓咱們爲不一樣的模塊寫本身的reducer
,最終將他們組合起來。好比咱們最開始那個牛奶倉庫,因爲咱們的業務發展很好,咱們又增長了一個放大米的倉庫,咱們能夠爲這兩個倉庫建立本身的reducer
,而後將他們組合起來,使用方法以下:
import { createStore, combineReducers } from 'redux'; const initMilkState = { milk: 0 }; function milkReducer(state = initMilkState, action) { switch (action.type) { case 'PUT_MILK': return {...state, milk: state.milk + action.count}; case 'TAKE_MILK': return {...state, milk: state.milk - action.count}; default: return state; } } const initRiceState = { rice: 0 }; function riceReducer(state = initRiceState, action) { switch (action.type) { case 'PUT_RICE': return {...state, rice: state.rice + action.count}; case 'TAKE_RICE': return {...state, rice: state.rice - action.count}; default: return state; } } // 使用combineReducers組合兩個reducer const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer}); let store = createStore(reducer); store.subscribe(() => console.log(store.getState())); // 操做🥛的action store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 1 store.dispatch({ type: 'PUT_MILK', count: 1 }); // milk: 2 store.dispatch({ type: 'TAKE_MILK', count: 1 }); // milk: 1 // 操做大米的action store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 1 store.dispatch({ type: 'PUT_RICE', count: 1 }); // rice: 2 store.dispatch({ type: 'TAKE_RICE', count: 1 }); // rice: 1
上面代碼咱們將大的state
分紅了兩個小的milkState
和riceState
,最終運行結果以下:
知道了用法,咱們嘗試本身來寫下呢!要手寫combineReducers
,咱們先來分析下他幹了啥,首先它的返回值是一個reducer
,這個reducer
一樣會做爲createStore
的參數傳進去,說明這個返回值是一個跟咱們以前普通reducer
結構同樣的函數。這個函數一樣接收state
和action
而後返回新的state
,只是這個新的state
要符合combineReducers
參數的數據結構。咱們嘗試來寫下:
function combineReducers(reducerMap) { const reducerKeys = Object.keys(reducerMap); // 先把參數裏面全部的鍵值拿出來 // 返回值是一個普通結構的reducer函數 const reducer = (state = {}, action) => { const newState = {}; for(let i = 0; i < reducerKeys.length; i++) { // reducerMap裏面每一個鍵的值都是一個reducer,咱們把它拿出來運行下就能夠獲得對應鍵新的state值 // 而後將全部reducer返回的state按照參數裏面的key組裝好 // 最後再返回組裝好的newState就行 const key = reducerKeys[i]; const currentReducer = reducerMap[key]; const prevState = state[key]; newState[key] = currentReducer(prevState, action); } return newState; }; return reducer; }
官方源碼的實現原理跟咱們的同樣,只是他有更多的錯誤處理,你們能夠對照着看下。
手寫applyMiddleware
middleware
是Redux裏面很重要的一個概念,Redux的生態主要靠這個API接入,好比咱們想寫一個logger
的中間件能夠這樣寫(這個中間件來自於官方文檔):
// logger是一箇中間件,注意返回值嵌了好幾層函數 // 咱們後面來看看爲何這麼設計 function logger(store) { return function(next) { return function(action) { console.group(action.type); console.info('dispatching', action); let result = next(action); console.log('next state', store.getState()); console.groupEnd(); return result } } } // 在createStore的時候將applyMiddleware做爲第二個參數傳進去 const store = createStore( reducer, applyMiddleware(logger) )
能夠看到上述代碼爲了支持中間件,createStore
支持了第二個參數,這個參數官方稱爲enhancer
,顧名思義他是一個加強器,用來加強store
的能力的。官方對於enhancer
的定義以下:
type StoreEnhancer = (next: StoreCreator) => StoreCreator
上面的結構的意思是說enhancer
做爲一個函數,他接收StoreCreator
函數做爲參數,同時返回的也必須是一個StoreCreator
函數。注意他的返回值也是一個StoreCreator
函數,也就是咱們把他的返回值拿出來繼續執行應該獲得跟以前的createStore
同樣的返回結構,也就是說咱們以前的createStore
返回啥結構,他也必須返回結構,也就是這個store
:
{ subscribe, dispatch, getState }
createStore
支持enhancer
根據他關於enhancer
的定義,咱們來改寫下本身的createStore
,讓他支持enhancer
:
function createStore(reducer, enhancer) { // 接收第二個參數enhancer // 先處理enhancer // 若是enhancer存在而且是函數 // 咱們將createStore做爲參數傳給他 // 他應該返回一個新的createStore給我 // 我再拿這個新的createStore執行,應該獲得一個store // 直接返回這個store就行 if(enhancer && typeof enhancer === 'function'){ const newCreateStore = enhancer(createStore); const newStore = newCreateStore(reducer); return newStore; } // 若是沒有enhancer或者enhancer不是函數,直接執行以前的邏輯 // 下面這些代碼都是以前那版 // 省略n行代碼 // ....... const store = { subscribe, dispatch, getState } return store; }
applyMiddleware
返回值是一個enhancer
前面咱們已經有了enhancer
的基本結構,applyMiddleware
是做爲第二個參數傳給createStore
的,也就是說他是一個enhancer
,準確的說是applyMiddleware
的返回值是一個enhancer
,由於咱們傳給createStore
的是他的執行結果applyMiddleware()
:
function applyMiddleware(middleware) { // applyMiddleware的返回值應該是一個enhancer // 按照咱們前面說的enhancer的參數是createStore function enhancer(createStore) { // enhancer應該返回一個新的createStore function newCreateStore(reducer) { // 咱們先寫個空的newCreateStore,直接返回createStore的結果 const store = createStore(reducer); return store } return newCreateStore; } return enhancer; }
實現applyMiddleware
上面咱們已經有了applyMiddleware
的基本結構了,可是功能還沒實現,要實現他的功能,咱們必須先搞清楚一箇中間件到底有什麼功能,仍是之前面的logger
中間件爲例:
function logger(store) { return function(next) { return function(action) { console.group(action.type); console.info('dispatching', action); let result = next(action); console.log('next state', store.getState()); console.groupEnd(); return result } } }
這個中間件運行效果以下:
能夠看到咱們let result = next(action);
這行執行以後state
改變了,前面咱們說了要改變state
只能dispatch(action)
,因此這裏的next(action)
就是dispatch(action)
,只是換了一個名字而已。並且注意最後一層返回值return function(action)
的結構,他的參數是action
,是否是很像dispatch(action)
,其實他就是一個新的dispatch(action)
,這個新的dispatch(action)
會調用原始的dispatch
,而且在調用的先後加上本身的邏輯。因此到這裏一箇中間件的結構也清楚了:
- 一箇中間件接收
store
做爲參數,會返回一個函數- 返回的這個函數接收老的
dispatch
函數做爲參數,會返回一個新的函數- 返回的新函數就是新的
dispatch
函數,這個函數裏面能夠拿到外面兩層傳進來的store
和老dispatch
函數
因此說白了,中間件就是增強dispatch
的功能,用新的dispatch
替換老的dispatch
,這不就是個裝飾者模式嗎?其實前面enhancer
也是一個裝飾者模式,傳入一個createStore
,在createStore
執行先後加上些代碼,最後又返回一個加強版的createStore
。
遵循這個思路,咱們的applyMiddleware
就能夠寫出來了:
// 直接把前面的結構拿過來 function applyMiddleware(middleware) { function enhancer(createStore) { function newCreateStore(reducer) { const store = createStore(reducer); // 將middleware拿過來執行下,傳入store // 獲得第一層函數 const func = middleware(store); // 解構出原始的dispatch const { dispatch } = store; // 將原始的dispatch函數傳給func執行 // 獲得加強版的dispatch const newDispatch = func(dispatch); // 返回的時候用加強版的newDispatch替換原始的dispatch return {...store, dispatch: newDispatch} } return newCreateStore; } return enhancer; }
照例用咱們本身的applyMiddleware
替換老的,跑起來是同樣的效果,說明咱們寫的沒問題,哈哈~
支持多個middleware
咱們的applyMiddleware
還差一個功能,就是支持多個middleware
,好比像這樣:
applyMiddleware( rafScheduler, timeoutScheduler, thunk, vanillaPromise, readyStatePromise, logger, crashReporter )
其實要支持這個也簡單,咱們返回的newDispatch
裏面依次的將傳入的middleware
拿出來執行就行,多個函數的串行執行可使用輔助函數compose
,這個函數定義以下。只是須要注意的是咱們這裏的compose
不能把方法拿來執行就完了,應該返回一個包裹了全部方法的方法。
function compose(...func){ return funcs.reduce((a, b) => (...args) => a(b(...args))); }
這個compose
可能比較讓人困惑,我這裏仍是講解下,好比咱們有三個函數,這三個函數都是咱們前面接收dispatch
返回新dispatch
的方法:
const fun1 = dispatch => newDispatch1; const fun2 = dispatch => newDispatch2; const fun3 = dispatch => newDispatch3;
當咱們使用了compose(fun1, fun2, fun3)
後執行順序是什麼樣的呢?
// 第一次其實執行的是 (func1, func2) => (...args) => func1(fun2(...args)) // 此次執行完的返回值是下面這個,用個變量存起來吧 const temp = (...args) => func1(fun2(...args)) // 咱們下次再循環的時候其實執行的是 (temp, func3) => (...args) => temp(func3(...args)); // 這個返回值是下面這個,也就是最終的返回值,其實就是從func3開始從右往左執行完了全部函數 // 前面的返回值會做爲後面參數 (...args) => temp(func3(...args)); // 再看看上面這個方法,若是把dispatch做爲參數傳進去會是什麼效果 (dispatch) => temp(func3(dispatch)); // 而後func3(dispatch)返回的是newDispatch3,這個又傳給了temp(newDispatch3),也就是下面這個會執行 (newDispatch3) => func1(fun2(newDispatch3)) // 上面這個裏面用newDispatch3執行fun2(newDispatch3)會獲得newDispatch2 // 而後func1(newDispatch2)會獲得newDispatch1 // 注意這時候的newDispatch1其實已經包含了newDispatch3和newDispatch2的邏輯了,將它拿出來執行這三個方法就都執行了
因此咱們支持多個middleware
的代碼就是這樣:
// 參數支持多箇中間件 function applyMiddleware(...middlewares) { function enhancer(createStore) { function newCreateStore(reducer) { const store = createStore(reducer); // 多個middleware,先解構出dispatch => newDispatch的結構 const chain = middlewares.map(middleware => middleware(store)); const { dispatch } = store; // 用compose獲得一個組合了全部newDispatch的函數 const newDispatchGen = compose(...chain); // 執行這個函數獲得newDispatch const newDispatch = newDispatchGen(dispatch); return {...store, dispatch: newDispatch} } return newCreateStore; } return enhancer; }
最後咱們再加一個logger2
中間件實現效果:
function logger2(store) { return function(next) { return function(action) { let result = next(action); console.log('logger2'); return result } } } let store = createStore(reducer, applyMiddleware(logger, logger2));
能夠看到logger2
也已經打印出來了,大功告成。
如今咱們也能夠知道他的中間件爲何要包裹幾層函數了:
第一層:目的是傳入
store
參數第二層:第二層的結構是
dispatch => newDispatch
,多箇中間件的這層函數能夠compose
起來,造成一個大的dispatch => newDispatch
第三層:這層就是最終的返回值了,其實就是
newDispatch
,是加強過的dispatch
,是中間件的真正邏輯所在。
到這裏咱們的applyMiddleware
就寫完了,對應的源碼能夠看這裏,相信看了本文再去看源碼就沒啥問題了!
本文全部代碼已經傳到GitHub,你們能夠去拿下來玩一下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux
總結
- 單純的Redux只是一個狀態機,
store
裏面存了全部的狀態state
,要改變裏面的狀態state
,只能dispatch action
。 - 對於發出來的
action
須要用reducer
來處理,reducer
會計算新的state
來替代老的state
。 subscribe
方法能夠註冊回調方法,當dispatch action
的時候會執行裏面的回調。- Redux其實就是一個發佈訂閱模式!
- Redux還支持
enhancer
,enhancer
其實就是一個裝飾者模式,傳入當前的createStore
,返回一個加強的createStore
。 - Redux使用
applyMiddleware
支持中間件,applyMiddleware
的返回值其實就是一個enhancer
。 - Redux的中間件也是一個裝飾者模式,傳入當前的
dispatch
,返回一個加強了的dispatch
。 - 單純的Redux是沒有View層的,因此他能夠跟各類UI庫結合使用,好比
react-redux
,計劃下一篇文章就是手寫react-redux
。
參考資料
GitHub源碼:https://github.com/reduxjs/redux
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
「前端進階知識」系列文章及示例源碼: https://github.com/dennis-jiang/Front-End-Knowledges
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~