Redux可是一個大名鼎鼎的庫,很多地方都在用,我也用了幾年了,今天這篇文章就是自己來實現一個Redux,以便於深入理解他的原理。我們還是老套路,從基本的用法入手,然后自己實現一個Redux來替代源碼的NPM包,但是功能保持不變。本文只會實現Redux的核心庫,跟其他庫的配合使用,比如React-Redux准備后面單獨寫一篇文章來講。有時候我們過於關注使用,只記住了各種使用方式,反而忽略了他們的核心原理,但是如果我們想真正的提高技術,最好還是一個一個搞清楚,比如Redux和React-Redux看起來很像,但是他們的核心理念和關注點是不同的,Redux其實只是一個單純狀態管理庫,沒有任何界面相關的東西,React-Redux關注的是怎么將Redux跟React結合起來,用到了一些React的API。
本文全部代碼已經上傳到GitHub,大家可以拿下來玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux
基本概念
Redux的概念有很多文章都講過,想必大家都看過很多了,我這里不再展開,只是簡單提一下。Redux基本概念主要有以下幾個:
Store
人如其名,Store就是一個倉庫,它存儲了所有的狀態(State),還提供了一些操作他的API,我們后續的操作其實都是在操作這個倉庫。假如我們的倉庫是用來放牛奶的,初始情況下,我們的倉庫里面一箱牛奶都沒有,那Store的狀態(State)就是:
{
milk: 0
}
Actions
一個Action就是一個動作,這個動作的目的是更改Store中的某個狀態,Store還是上面的那個倉庫,現在我想往倉庫放一箱牛奶,那"我想往倉庫放一箱牛奶"就是一個Action,代碼就是這樣:
{
type: "PUT_MILK",
count: 1
}
Reducers
前面"我想往倉庫放一箱牛奶"只是想了,還沒操作,具體操作要靠Reducer,Reducer就是根據接收的Action來改變Store中的狀態,比如我接收了一個PUT_MILK
,同時數量count
是1,那放進去的結果就是milk
增加了1,從0變成了1,代碼就是這樣:
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中對應的狀態。
簡單例子
下面我們來看一個簡單的例子,包含了前面提到的Store,Action和Reducer這幾個概念:
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
上。
看看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://juejin.im/post/5e3ffc85518825494e2772fd
“前端進階知識”系列文章源碼GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges