自己动手从零实现Redux(一)

“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.” —Martin Golding

前言

Redux 官方文档已经详细介绍了Redux的动机、核心概念以及背后的设计思想,这里就简单总结下不再赘述:

  • Redux是一个可预测的状态管理工具
  • Redux的根本目的是通过对更新的方式和时间进行规范限制,使得状态的变化变得可预测。它借鉴了“Flux”的思想,将更新逻辑抽离出来,并使用原生JS对象"action"来描述需要发生的更改。
  • Redux的三大基本原则: 唯一的Store、状态为只读、纯函数来做改变

Redux基本工作流

Redux基本工作流

createStore

createStore是redux的核心函数,主要提供了getStatesubscribedispatch三个API。其中通过getState可以读取当前的状态,返回唯一的store;subscribe是为了订阅监听store的变化,实质就是订阅发布模式,订阅后当store发生变化时通过调用监听函数来通知订阅者。dispatch函数接收一个名为”action”的参数,redux规定该参数是用来描述变化的原生JS对象,dispatch主要的作用就是将”action”派发给reducer,然后reducer根据当前的状态以及”action”来计算出新的状态,所以真正改变状态是reducer。

根据上面的描述,我们来实现一个简易的createStore。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function createStore(reducer, initialState){
let currentState = initialState;
let currentReducer = reducer;
let isDispatching = false;
let listeners = [];

function getState(){
if(!isDispatching){
return currentState;
}
}

function subscribe(listener){
listeners.push(listener);
return function unsubscribe(){
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
}
}

function dispatch(action){
try {
isDispatching = true;
const newState = currentReducer(currentState, action);
currentState = newState;
} finally {
isDispatching = false;
}
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}

return {
getState,
subscribe,
dispatch,
}
}

上面的代码浅显易懂,现在来测试下这个简易的createStore是否能正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const initialState = {
count: 0
};
const reducer = (oldState, action) => {
const {
type,
payload,
} = action;
let newState;
switch(type){
case 'INCREMENT':
newState = {
count: oldState.count + payload
};
break;
case 'DECREMENT':
newState = {
count: oldState.count - payload
};
break;
default:
newState = oldState;
}
return newState;
}
const store = createStore(reducer, initialState);

function subscribe1(){
let state = store.getState();
console.log('subscribe1', state.count);
}

function subscribe2(){
let state = store.getState();
console.log('subscribe2', state.count);
}

function subscribe3(){
let state = store.getState();
console.log('subscribe3', state.count);
}

const unsubscribe1 = store.subscribe(subscribe1);
const unsubscribe2 = store.subscribe(subscribe2);
const unsubscribe3 = store.subscribe(subscribe3);

store.dispatch({
type: 'INCREMENT',
payload: 3,
});

store.dispatch({
type: 'DECREMENT',
payload: 5,
});

/* console
------------
subscribe1 3
subscribe2 3
subscribe3 3
subscribe1 -2
subscribe2 -2
subscribe3 -2
------------
*/

上面我们添加了3个监听函数,当dispatch一个action给reducer,其根据action改变状态后,3个监听函数被依次执行了,合情合理;既然允许订阅,那肯定支持退订了。从代码可以看出subscribe函数返回值就是退订函数,我们通过调用它即可退订。这里假设我们只想订阅subscribe2一次,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function subscribe2(){
let state = store.getState();
console.log('subscribe2', state.count);
+ unsubscribe2()
}

/* console
------------
subscribe1 3
subscribe2 3
subscribe1 -2
subscribe3 -2
------------
*/

😫😫😫😫😫
好像出现了问题,第三个订阅函数竟然没有执行。

1
2
3
4
5
6
7
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
// i为1时调用subscribe2后
// listeners为[subscribe1, subscribe3]
}
// 所以没有执行了subscribe3

现实中,如果我们在listener函数执行时添加或取消listener,不会影响本次dispatch过程,即这些操作只能在下一次dispatch过程中生效;所以我们需要变量来保存下一次的listeners;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function createStore(reducer, initialState){
let currentState = initialState;
let currentReducer = reducer;
let isDispatching = false;
- let listeners = [];
+ let currentListeners = [];
+ let nextListeners = [];

function getState(){
if(!isDispatching){
return currentState;
}
}

function subscribe(listener){
- listeners.push(listener);
+ if(currentListeners === nextListeners){
+ nextListeners = currentListeners.slice();
+ }
+ nextListeners.push(listener);
return function unsubscribe(){
- const index = listeners.indexOf(listener);
- listeners.splice(index, 1);
+ if(currentListeners === nextListeners){
+ nextListeners = currentListeners.slice();
+ }
+ const index = nextListeners.indexOf(listener);
+ nextListeners.splice(index, 1);
}
}

function dispatch(action){
try {
isDispatching = true;
const newState = currentReducer(currentState, action);
currentState = newState;
} finally {
isDispatching = false;
}
+ const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}

return {
getState,
subscribe,
dispatch,
}
}

现在用了两个变量来保存监听函数,保证了订阅或取消订阅操作只会在下一次dispatch中生效。我们用上面的示例测试输出如下:

1
2
3
4
5
6
7
8
9
/* console
------------
subscribe1 3
subscribe2 3
subscribe3 3
subscribe1 -2
subscribe3 -2
------------
*/

😄😄😄😄😄

拆分reducer

Redux坚持唯一store的原则,也就意味着整个应用的state都保存单一的对象中。而我们知道reducer的职责就是根据”action”从旧的state计算出新的state。所以我们需要为应用中发生的所有”action“编写相应的”reducer”,就像的测试示例一样,我们需要在reducer中处理”INCREMENT“和“DECREMENT“,如果还有其它“action”,我们就需要在reducer中继续添加相应的处理逻辑,想象一下如果我们在一个reducer函数中处理整个应用中可能成千上万个“action”,那reducer函数会有多庞大;这显然不合理。所以我们必须拆分reducer,那按照何种方式拆分呢?我们知道尽管所有的state保存在一个对象里面,但其实这些state之间大部分是完全独立的,所以我们可以根据state的独立性来划分reducer,这样每个reducer就可以只需要管理对应的state;

下面我们来管理下两个完全独立的state:counter和todoList;它们有各自的reducer,并且reducer只会管自己对应的state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 既然每个reducer分别管理各自部分state,那完全可以把初始值放在reducer里面
// 初始counter值
const initCounter = 0;
// counterReducer只会接受counter state
function counterReducer(state = initCounter , action){
let newState;
switch(action.type){
case 'INCREMENT':
newState = state + action.payload;
break;
case 'DECREMENT':
newState = state - action.payload;
break;
default:
newState = state;
}
return newState;
}

// 初始todoLiist值
const initTodoList = ["eat", "sleep", "play"];
// todoListReducer只会接受todoList state
function todoListReducer(state = initTodoList, action){
switch (action.type) {
case 'ADD_TODO':
return [
...state,
action.payload,
]
default:
return state
}
}

现在我们已经有了两个reducer,它们能独立更新各自管理的state。那么新的问题来了,我们如何将这两个reducer组合起来确保它们能对派发的action作出响应,各自计算出新的state,并且将这些部分state合并为整个state。这时候我们就需要combineReducers

⚠️: 拆分reducer时,我们完全可以将各部分state的初始值放在了对应的reducer中作为默认参数。这样在我们调用createStore时就无需传入初始值了。但是为了能拿到默认初始值,我们必须添加一行代码:

1
2
3
4
5
6
7
8
9
> // ~~~ 省略的代码
> // 在createStore返回前添加
> + dispatch({ type: Symbol() });
> return {
> subscribe,
> dispatch,
> getState
> };
>

这里我们在createStore执行返回前通过dispatch派发一个type为Symbol()的action,这意味着其不会匹配任何reducer里的action,因此所有的reducer都进入default项,即返回默认初始值。

combineReducers

根据上面的问题,我们大致能分析出combineReducers应该是一个函数,它主要的功能就是合并所有的reducer返回一个新的reducer;新的reducer可以接受整个state以及action,并且能把新的state合并为整个store。下面我们尝试实现一个combineReducers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function combineReducers(reducers){
const reducerKeys = Object.keys(reducers);
// reducer必须是函数 finalReducers为筛选过后最终的reducers
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
// 返回新的reducer
return function combination(state={}, action){
const nextState = {};
// 接收到action时,调用每一个reducer
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[key];
const previousStateForKey = state[key];
// 每一个reducer,根据各自对应的state和action,计算出新的state
const nextStateForKey = reducer(previousStateForKey, action);
// 合并为新的整个的state
nextState[key] = nextStateForKey;
}
return nextState;
}
}

现在我们利用之前的两个reducer来感受下combineReducers的威力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const reducer = combineReducers({
counter: counterReducer,
todoList: todoListReducer,
});
// 无需传入初始值了
let store = createStore(reducer);
store.subscribe(() => {
let state = store.getState();
console.log(state.counter, state.todoList);
});

store.dispatch({
type: 'INCREMENT',
payload: 12,
});

store.dispatch({
type: 'ADD_TODO',
payload: 'coding',
});

/* console
------------
12 [ 'eat', 'sleep', 'play' ]
12 [ 'eat', 'sleep', 'play', 'coding' ]
------------
*/

😄😄😄😄😄

actionCreator

Redux强调任何需要改变state的操作都必须通过dispatch派发描述如何改变的action通知reducer更新state来完成,因此项目中往往会大量调用dispatch派发action,而action是一个原生JS对象,其中字段type是与reducer协定的,静态的,而字段payload是动态的,所以我们可以封装一个actionCreator函数来产生action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addTodoItem(todoItem){
return {
type: 'ADD_TODO',
payload: todoItem,
}
}

function counterIncrement(count){
return {
type: 'INCREMENT',
payload: count,
}
}
// 使用
store.dispatch(counterIncrement(12));
store.dispatch(addTodoItem('coding'));

上面的代码里,我们已经封装了两个actionCreator: counterIncrementaddTodoItem,我们每次dispatch时可以直接调用它们以产生我们需要的action;尽管这样我们还是需要每次显示调用store.dispatch函数,而且从易用性及扩展性方面上来说,页面组件只需接受相应的状态及改变状态的方法,而不应该感知到Redux的存在。所以我们还需要一个bindActionCreators函数将dispatch和actionCreator封装起来。

bindActionCreators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function bindActionCreators(anctionCreators, dispatch){
const boundActionCreators = {};
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
boundActionCreators[key] = (...args) => {
dispatch(actionCreator(args));
}
}
}
return boundActionCreators;
}

// 使用
const mutations = bindActionCreators(
{
counterIncrement,
addTodoItem,
},
store.diapatch
);

mutations.counterIncrement(12);
mutations.addTodoItem('coding');
// 现在上面的代码等价于以下
/*
store.dispatch({
type: 'INCREMENT',
payload: 12,
});

store.dispatch({
type: 'ADD_TODO',
payload: 'coding',
});
*/

结束语

到这里为止,我们已经实现了一个像模像样的Redux!