手寫一個Redux,深入理解其原理
阿新 • • 發佈:2020-07-03
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](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux)**
## 基本概念
Redux的概念有很多文章都講過,想必大家都看過很多了,我這裡不再展開,只是簡單提一下。Redux基本概念主要有以下幾個:
### Store
人如其名,Store就是一個倉庫,它儲存了所有的狀態(State),還提供了一些操作他的API,我們後續的操作其實都是在操作這個倉庫。假如我們的倉庫是用來放牛奶的,初始情況下,我們的倉庫裡面一箱牛奶都沒有,那Store的狀態(State)就是:
```json
{
milk: 0
}
```
### Actions
一個Action就是一個動作,這個動作的目的是更改Store中的某個狀態,Store還是上面的那個倉庫,現在我想往倉庫放一箱牛奶,那"我想往倉庫放一箱牛奶"就是一個Action,程式碼就是這樣:
```javascript
{
type: "PUT_MILK",
count: 1
}
```
### Reducers
前面"我想往倉庫放一箱牛奶"只是想了,還沒操作,具體操作要靠Reducer,Reducer就是根據接收的Action來改變Store中的狀態,比如我接收了一個`PUT_MILK`,同時數量`count`是1,那放進去的結果就是`milk`增加了1,從0變成了1,程式碼就是這樣:
```javascript
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這幾個概念:
```javascript
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`觸發回撥,想到了什麼,這不就是釋出訂閱模式嗎?[我之前有一篇文章詳細講過釋出訂閱模式了,這裡直接仿寫一個。](https://juejin.im/post/5e7978485188255e237c2a29)
```javascript
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`改寫如下:
```javascript
function dispatch(action) {
state = reducer(state, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
```
到這裡,前面例子用到的所有API我們都自己實現了,我們用自己的Redux來替換下官方的Redux試試:
```javascript
// import { createStore } from 'redux';
import { createStore } from './myRedux';
```
可以看到輸出結果是一樣的,說明我們自己寫的Redux沒有問題:
![image-20200630152344176](https://user-gold-cdn.xitu.io/2020/7/3/173133eb43fc7291?w=152&h=73&f=png&s=2774)
瞭解了Redux的核心原理,我們再去看他的原始碼應該就沒有問題了,[createStore的原始碼傳送門。](https://github.com/reduxjs/redux/blob/master/src/createStore.ts)
最後我們再來梳理下Redux的核心流程,注意單純的Redux只是個狀態機,是沒有`View`層的哦。
![image-20200630154356840](https://user-gold-cdn.xitu.io/2020/7/3/173133ef6dd2437f?w=888&h=506&f=png&s=38347)
除了這個核心邏輯外,Redux裡面還有些API也很有意思,我們也來手寫下。
## 手寫`combineReducers`
`combineReducers`也是使用非常廣泛的API,當我們應用越來越複雜,如果將所有邏輯都寫在一個`reducer`裡面,最終這個檔案可能會有成千上萬行,所以Redux提供了`combineReducers`,可以讓我們為不同的模組寫自己的`reducer`,最終將他們組合起來。比如我們最開始那個牛奶倉庫,由於我們的業務發展很好,我們又增加了一個放大米的倉庫,我們可以為這兩個倉庫建立自己的`reducer`,然後將他們組合起來,使用方法如下:
```javascript
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()));
// 操作