手寫一Redux,深入理解其原理
Redux可是一個大名鼎鼎的庫,很多地方都在用,我也用了幾年了,今天這篇文章就是自己來實現(xiàn)一個Redux,以便于深入理解他的原理。我們還是老套路,從基本的用法入手,然后自己實現(xiàn)一個Redux來替代源碼的NPM包,但是功能保持不變。本文只會實現(xiàn)Redux的核心庫,跟其他庫的配合使用,比如React-Redux準備后面單獨寫一篇文章來講。有時候我們過于關注使用,只記住了各種使用方式,反而忽略了他們的核心原理,但是如果我們想真正的提高技術,最好還是一個一個搞清楚,比如Redux和React-Redux看起來很像,但是他們的核心理念和關注點是不同的,Redux其實只是一個單純狀態(tài)管理庫,沒有任何界面相關的東西,React-Redux關注的是怎么將Redux跟React結合起來,用到了一些React的API。
本文全部代碼已經(jīng)上傳到GitHub,大家可以拿下來玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux
基本概念
Redux的概念有很多文章都講過,想必大家都看過很多了,我這里不再展開,只是簡單提一下。Redux基本概念主要有以下幾個:
Store
人如其名,Store就是一個倉庫,它存儲了所有的狀態(tài)(State),還提供了一些操作他的API,我們后續(xù)的操作其實都是在操作這個倉庫。假如我們的倉庫是用來放牛奶的,初始情況下,我們的倉庫里面一箱牛奶都沒有,那Store的狀態(tài)(State)就是:
- {
- milk: 0
- }
Actions
一個Action就是一個動作,這個動作的目的是更改Store中的某個狀態(tài),Store還是上面的那個倉庫,現(xiàn)在我想往倉庫放一箱牛奶,那"我想往倉庫放一箱牛奶"就是一個Action,代碼就是這樣:
- {
- type: "PUT_MILK",
- count: 1
- }
Reducers
前面"我想往倉庫放一箱牛奶"只是想了,還沒操作,具體操作要靠Reducer,Reducer就是根據(jù)接收的Action來改變Store中的狀態(tài),比如我接收了一個PUT_MILK,同時數(shù)量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本身就是一個單純的狀態(tài)機,Store存放了所有的狀態(tài),Action是一個改變狀態(tài)的通知,Reducer接收到通知就更改Store中對應的狀態(tài)。
簡單例子
下面我們來看一個簡單的例子,包含了前面提到的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發(fā)生了變化,傳入的回調(diào)函數(shù)就會被調(diào)用
- // 如果是結合頁面更新,更新的操作就是在這里執(zhí)行
- store.subscribe(() => console.log(store.getState()));
- // 將action發(fā)出去要用dispatch
- store.dispatch({ type: 'PUT_MILK' }); // milk: 1
- store.dispatch({ type: 'PUT_MILK' }); // milk: 2
- store.dispatch({ type: 'TAKE_MILK' }); // milk: 1
自己實現(xiàn)
前面我們那個例子雖然短小,但是已經(jīng)包含了Redux的核心功能了,所以我們手寫的第一個目標就是替換這個例子中的Redux。要替換這個Redux,我們得先知道他里面都有什么東西,仔細一看,我們好像只用到了他的一個API:
createStore:這個API接受reducer方法作為參數(shù),返回一個store,主要功能都在這個store上。
看看store上我們都用到了啥:
store.subscribe: 訂閱state的變化,當state變化的時候執(zhí)行回調(diào),可以有多個subscribe,里面的回調(diào)會依次執(zhí)行。
store.dispatch: 發(fā)出action的方法,每次dispatch action都會執(zhí)行reducer生成新的state,然后執(zhí)行subscribe注冊的回調(diào)。
store.getState:一個簡單的方法,返回當前的state。
看到subscribe注冊回調(diào),dispatch觸發(fā)回調(diào),想到了什么,這不就是發(fā)布訂閱模式嗎?我之前有一篇文章詳細講過發(fā)布訂閱模式了,這里直接仿寫一個。
- function createStore() {
- let state; // state記錄所有狀態(tài)
- let listeners = []; // 保存所有注冊的回調(diào)
- function subscribe(callback) {
- listeners.push(callback); // subscribe就是將回調(diào)保存下來
- }
- // dispatch就是將所有的回調(diào)拿出來依次執(zhí)行就行
- 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核心也是一個發(fā)布訂閱模式,就是這么簡單!等等,好像漏了啥,reducer呢?reducer的作用是在發(fā)布事件的時候改變state,所以我們的dispatch在執(zhí)行回調(diào)前應該先執(zhí)行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我們都自己實現(xiàn)了,我們用自己的Redux來替換下官方的Redux試試:
- // import { createStore } from 'redux';
- import { createStore } from './myRedux';
可以看到輸出結果是一樣的,說明我們自己寫的Redux沒有問題:
了解了Redux的核心原理,我們再去看他的源碼應該就沒有問題了,createStore的源碼傳送門。
最后我們再來梳理下Redux的核心流程,注意單純的Redux只是個狀態(tài)機,是沒有View層的哦。
除了這個核心邏輯外,Redux里面還有些API也很有意思,我們也來手寫下。
手寫combineReducers
combineReducers也是使用非常廣泛的API,當我們應用越來越復雜,如果將所有邏輯都寫在一個reducer里面,最終這個文件可能會有成千上萬行,所以Redux提供了combineReducers,可以讓我們?yōu)椴煌哪K寫自己的reducer,最終將他們組合起來。比如我們最開始那個牛奶倉庫,由于我們的業(yè)務發(fā)展很好,我們又增加了一個放大米的倉庫,我們可以為這兩個倉庫創(chuàng)建自己的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的參數(shù)傳進去,說明這個返回值是一個跟我們之前普通reducer結構一樣的函數(shù)。這個函數(shù)同樣接收state和action然后返回新的state,只是這個新的state要符合combineReducers參數(shù)的數(shù)據(jù)結構。我們嘗試來寫下:
- function combineReducers(reducerMap) {
- const reducerKeys = Object.keys(reducerMap); // 先把參數(shù)里面所有的鍵值拿出來
- // 返回值是一個普通結構的reducer函數(shù)
- const reducer = (state = {}, action) => {
- const newState = {};
- for(let i = 0; i < reducerKeys.length; i++) {
- // reducerMap里面每個鍵的值都是一個reducer,我們把它拿出來運行下就可以得到對應鍵新的state值
- // 然后將所有reducer返回的state按照參數(shù)里面的key組裝好
- // 最后再返回組裝好的newState就行
- const key = reducerKeys[i];
- const currentReducer = reducerMap[key];
- const prevState = state[key];
- newState[key] = currentReducer(prevState, action);
- }
- return newState;
- };
- return reducer;
- }
官方源碼的實現(xiàn)原理跟我們的一樣,只是他有更多的錯誤處理,大家可以對照著看下。
手寫applyMiddleware
middleware是Redux里面很重要的一個概念,Redux的生態(tài)主要靠這個API接入,比如我們想寫一個logger的中間件可以這樣寫(這個中間件來自于官方文檔):
- // logger是一個中間件,注意返回值嵌了好幾層函數(shù)
- // 我們后面來看看為什么這么設計
- 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的時候?qū)pplyMiddleware作為第二個參數(shù)傳進去
- const store = createStore(
- reducer,
- applyMiddleware(logger)
- )
可以看到上述代碼為了支持中間件,createStore支持了第二個參數(shù),這個參數(shù)官方稱為enhancer,顧名思義他是一個增強器,用來增強store的能力的。官方對于enhancer的定義如下:
- type StoreEnhancer = (next: StoreCreator) => StoreCreator
上面的結構的意思是說enhancer作為一個函數(shù),他接收StoreCreator函數(shù)作為參數(shù),同時返回的也必須是一個StoreCreator函數(shù)。注意他的返回值也是一個StoreCreator函數(shù),也就是我們把他的返回值拿出來繼續(xù)執(zhí)行應該得到跟之前的createStore一樣的返回結構,也就是說我們之前的createStore返回啥結構,他也必須返回結構,也就是這個store:
- {
- subscribe,
- dispatch,
- getState
- }
createStore支持enhancer
根據(jù)他關于enhancer的定義,我們來改寫下自己的createStore,讓他支持enhancer:
- function createStore(reducer, enhancer) { // 接收第二個參數(shù)enhancer
- // 先處理enhancer
- // 如果enhancer存在并且是函數(shù)
- // 我們將createStore作為參數(shù)傳給他
- // 他應該返回一個新的createStore給我
- // 我再拿這個新的createStore執(zhí)行,應該得到一個store
- // 直接返回這個store就行
- if(enhancer && typeof enhancer === 'function'){
- const newCreateStore = enhancer(createStore);
- const newStore = newCreateStore(reducer);
- return newStore;
- }
- // 如果沒有enhancer或者enhancer不是函數(shù),直接執(zhí)行之前的邏輯
- // 下面這些代碼都是之前那版
- // 省略n行代碼
- // .......
- const store = {
- subscribe,
- dispatch,
- getState
- }
- return store;
- }
applyMiddleware返回值是一個enhancer
前面我們已經(jīng)有了enhancer的基本結構,applyMiddleware是作為第二個參數(shù)傳給createStore的,也就是說他是一個enhancer,準確的說是applyMiddleware的返回值是一個enhancer,因為我們傳給createStore的是他的執(zhí)行結果applyMiddleware():
- function applyMiddleware(middleware) {
- // applyMiddleware的返回值應該是一個enhancer
- // 按照我們前面說的enhancer的參數(shù)是createStore
- function enhancer(createStore) {
- // enhancer應該返回一個新的createStore
- function newCreateStore(reducer) {
- // 我們先寫個空的newCreateStore,直接返回createStore的結果
- const store = createStore(reducer);
- return store
- }
- return newCreateStore;
- }
- return enhancer;
- }
實現(xiàn)applyMiddleware
上面我們已經(jīng)有了applyMiddleware的基本結構了,但是功能還沒實現(xiàn),要實現(xiàn)他的功能,我們必須先搞清楚一個中間件到底有什么功能,還是以前面的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);這行執(zhí)行之后state改變了,前面我們說了要改變state只能dispatch(action),所以這里的next(action)就是dispatch(action),只是換了一個名字而已。而且注意最后一層返回值return function(action)的結構,他的參數(shù)是action,是不是很像dispatch(action),其實他就是一個新的dispatch(action),這個新的dispatch(action)會調(diào)用原始的dispatch,并且在調(diào)用的前后加上自己的邏輯。所以到這里一個中間件的結構也清楚了:
- 一個中間件接收store作為參數(shù),會返回一個函數(shù)
- 返回的這個函數(shù)接收老的dispatch函數(shù)作為參數(shù),會返回一個新的函數(shù)
- 返回的新函數(shù)就是新的dispatch函數(shù),這個函數(shù)里面可以拿到外面兩層傳進來的store和老dispatch函數(shù)
所以說白了,中間件就是加強dispatch的功能,用新的dispatch替換老的dispatch,這不就是個裝飾者模式嗎?其實前面enhancer也是一個裝飾者模式,傳入一個createStore,在createStore執(zhí)行前后加上些代碼,最后又返回一個增強版的createStore。可見設計模式在這些優(yōu)秀的框架中還真是廣泛存在,如果你對裝飾者模式還不太熟悉,可以看我之前這篇文章。
遵循這個思路,我們的applyMiddleware就可以寫出來了:
- // 直接把前面的結構拿過來
- function applyMiddleware(middleware) {
- function enhancer(createStore) {
- function newCreateStore(reducer) {
- const store = createStore(reducer);
- // 將middleware拿過來執(zhí)行下,傳入store
- // 得到第一層函數(shù)
- const func = middleware(store);
- // 解構出原始的dispatch
- const { dispatch } = store;
- // 將原始的dispatch函數(shù)傳給func執(zhí)行
- // 得到增強版的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拿出來執(zhí)行就行,多個函數(shù)的串行執(zhí)行可以使用輔助函數(shù)compose,這個函數(shù)定義如下。只是需要注意的是我們這里的compose不能把方法拿來執(zhí)行就完了,應該返回一個包裹了所有方法的方法。
- function compose(...func){
- return funcs.reduce((a, b) => (...args) => a(b(...args)));
- }
這個compose可能比較讓人困惑,我這里還是講解下,比如我們有三個函數(shù),這三個函數(shù)都是我們前面接收dispatch返回新dispatch的方法:
- const fun1 = dispatch => newDispatch1;
- const fun2 = dispatch => newDispatch2;
- const fun3 = dispatch => newDispatch3;
當我們使用了compose(fun1, fun2, fun3)后執(zhí)行順序是什么樣的呢?
- // 第一次其實執(zhí)行的是
- (func1, func2) => (...args) => func1(fun2(...args))
- // 這次執(zhí)行完的返回值是下面這個,用個變量存起來吧
- const temp = (...args) => func1(fun2(...args))
- // 我們下次再循環(huán)的時候其實執(zhí)行的是
- (temp, func3) => (...args) => temp(func3(...args));
- // 這個返回值是下面這個,也就是最終的返回值,其實就是從func3開始從右往左執(zhí)行完了所有函數(shù)
- // 前面的返回值會作為后面參數(shù)
- (...args) => temp(func3(...args));
- // 再看看上面這個方法,如果把dispatch作為參數(shù)傳進去會是什么效果
- (dispatch) => temp(func3(dispatch));
- // 然后func3(dispatch)返回的是newDispatch3,這個又傳給了temp(newDispatch3),也就是下面這個會執(zhí)行
- (newDispatch3) => func1(fun2(newDispatch3))
- // 上面這個里面用newDispatch3執(zhí)行fun2(newDispatch3)會得到newDispatch2
- // 然后func1(newDispatch2)會得到newDispatch1
- // 注意這時候的newDispatch1其實已經(jīng)包含了newDispatch3和newDispatch2的邏輯了,將它拿出來執(zhí)行這三個方法就都執(zhí)行了
更多關于compose原理的細節(jié)可以看我之前這篇文章。
所以我們支持多個middleware的代碼就是這樣:
- // 參數(shù)支持多個中間件
- 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的函數(shù)
- const newDispatchGen = compose(...chain);
- // 執(zhí)行這個函數(shù)得到newDispatch
- const newDispatch = newDispatchGen(dispatch);
- return {...store, dispatch: newDispatch}
- }
- return newCreateStore;
- }
- return enhancer;
- }
最后我們再加一個logger2中間件實現(xiàn)效果:
- 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也已經(jīng)打印出來了,大功告成。
現(xiàn)在我們也可以知道他的中間件為什么要包裹幾層函數(shù)了:
第一層:目的是傳入store參數(shù)
第二層:第二層的結構是dispatch => newDispatch,多個中間件的這層函數(shù)可以compose起來,形成一個大的dispatch => newDispatch
第三層:這層就是最終的返回值了,其實就是newDispatch,是增強過的dispatch,是中間件的真正邏輯所在。
到這里我們的applyMiddleware就寫完了,對應的源碼可以看這里,相信看了本文再去看源碼就沒啥問題了!
本文所有代碼已經(jīng)傳到GitHub,大家可以去拿下來玩一下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux
總結
- 單純的Redux只是一個狀態(tài)機,store里面存了所有的狀態(tài)state,要改變里面的狀態(tài)state,只能dispatch action。
- 對于發(fā)出來的action需要用reducer來處理,reducer會計算新的state來替代老的state。
- subscribe方法可以注冊回調(diào)方法,當dispatch action的時候會執(zhí)行里面的回調(diào)。
- Redux其實就是一個發(fā)布訂閱模式!
- Redux還支持enhancer,enhancer其實就是一個裝飾者模式,傳入當前的createStore,返回一個增強的createStore。
- Redux使用applyMiddleware支持中間件,applyMiddleware的返回值其實就是一個enhancer。
- Redux的中間件也是一個裝飾者模式,傳入當前的dispatch,返回一個增強了的dispatch。
- 單純的Redux是沒有View層的,所以他可以跟各種UI庫結合使用,比如react-redux,計劃下一篇文章就是手寫react-redux。