从 0 实现一个 mini redux

前言

本文从 redux 原理出发,一步步实现一个自己的 mini-redux,主要目的是了解其内部之间的各种关系,所以本篇不会讲解太多关于 redux 的用法

redux 是什么

redux 是一种可预测的状态管理库,在 react 中,它解决的是多个组件之间的通信问题

在没有使用 redux 的情况下,如果两个组件(非父子关系)之间需要通信的话,可能需要多个中间组件来为他们进行消息传递,这样既浪费了资源,代码也会变得更复杂

redux 提出了单一数据源 store 来存储状态数据,所有的组件都可以通过 action 来修改 store,也可以从 store 中获取最新状态。使用了 redux 就可以完美解决组件之间的通信问题

redux 的设计原则

redux 的三大设计原则:

  • 单一数据源
  • 状态是只读的
  • 使用纯函数编写 reducer

单一数据源

意思是整个 react 项目里的 state 都存放在一起,单一数据源主要是为了解决状态一致性的问题

在传统的 MVC 架构中,需要创建无数个 Model,而 Model 之间可以互相监听、触发事件甚至循环或嵌套触发事件,这些在 redux 中都是不允许的

在 redux 的思想里,一个应用永远只有唯一的数据源,这个设计也是有一些好处的,对于开发者来说,它可以更容易调试和观察状态的变化

也不用担心数据源对象过于庞大的问题,redux 提供的 combineReducers 函数可以解决这个问题

状态是只读的

这里说的状态,指的是上面说的存放在 store 中的状态数据,你不能直接对其中的状态数据进行改动只能间接的通过发送 action 来改动状态。间接的改动状态,这是一个很关键的设计,也是单向数据流的重点之一,对于每个动作的发生,最终会影响到什么状态上的改动,一个接一个的执行顺序等等,都是可预测的

使用纯函数编写 reducer

纯函数的概念:函数的返回结果只依赖其参数,并且执行过程中不会产生副作用

在 redux 中,我们通过定义 reducer 来更改状态,每个 reducer 都是纯函数,这意味着它没有副作用,相同的输入必定有相同的输出

ps:修改外部的变量、调用 DOM API 修改页面,发送 Ajax 请求,调用 window.reload 刷新浏览器甚至是console.log 打印数据,都是副作用

就问你纯不纯

redux 的几个基本概念

store

store 是存储数据的地方,它是一个对象,有这么几个方法

  • getState() 获取当前状态

  • dispatch(action) 派发 action

  • subscribe(handler) 监听数据的变化

action

action 可以理解为操作数据的行为

action 一般的写法如下:

1
2
3
4
5
6
const add = (val) => {
return {
type: 'ADD',
value: val
}
}

通过 type 去定义这个 action 是干嘛的,在 reducer 中要进行什么操作

dispatch

dispatch 的作用就是派发一个 action,让 reducer 进行数据的处理

一般写法:

1
2
3
4
dispatch({
type: 'ADD',
value: 1
})

reducer

reducer 里是真正更改数据的地方,dispatch 派发的 action 最终由 reducer 来进行数据的处理,并且每次的更改都是返回新的 state,这样做的目的是为了让 state 变的可预测

middleware

在创建 store 的时候 createStore 可以传入三个参数,第三个参数就是中间件,使用 redux 提供的 applyMiddleware 方法来调用,applyMiddleware 等于是对 dispatch 进行了增强,这样的话,在 dispatch 的过程中可以做一些其他的事情,比如记录 state 的变化、异步请求等等

从 0 实现一个 mini-redux

redux 的核心,就是 createStore 这个函数,store、getState、dispatch 都是这个函数返回的

redux 的大致原理就是发布订阅模式:通过 dispatch 派发 action 更改 store,通过 subscribe 订阅 store 的变化,去更新对应的 view

createStore

用过 createStore 方法的都知道,创建一个 store 需要三个参数

1
2
3
4
5
6
7
8
9
/**
* 创建 store
* @param {*} reducer
* @param {*} initState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initState, enhancer) => {

}

这个函数会返回几个功能函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建 store
* @param {*} reducer
* @param {*} initialState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initialState, enhancer) => {
return {
getState,
dispatch,
subscribe,
replaceReducer
}
}

下面来实现这几个方法

getState 的实现

getState 方法的作用就是返回当前的 state

1
2
3
4
5
6
7
let currentState; // 当前 state
/**
* 返回最新的 state
*/
const getState = () => {
return currentState;
};

subscribe 的实现

subscribe 的作用是订阅 state 的变化,使用者通过这个方法订阅,当 state 变化后,执行监听函数subscribe 是一个高阶函数,它的返回值一个函数,执行该函数可以移除当前的监听函数

1
2
3
4
5
6
7
8
9
10
11
12
13
let subQueue = []; // 创建一个监听队列
/**
* 监听 state 的变动
* @param {*} listener 数据变化时要执行的函数
*/
const subscribe = (listener) => {
// 把监听函数放入监听队列里
subQueue.push(listener);
// 移除监听事件
return () => {
subQueue = subQueue.filter((l) => l !== listener);
};
};

dispatch 的实现

dispatch 方法接收一个 action 参数,来执行 reducer,执行完成后会执行所有的监听函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let currentReducer = reducer;
let isDispatch = false;
/**
* 派发 action 并执行所有 监听函数
* @param {*} action
*/
const dispatch = (action) => {
// 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
if(isDispatch) {
throw new Error('dispatching')
}

try {
currentState = currentReducer(currentState, action)
isDispatch = true;
} finally {
isDispatch = false;
}

// 执行所有监听函数
subQueue.forEach((listener) => listener())
return action
}

replaceReducer 的实现

replaceReducer 的作用就是替换当前 reducer,执行 createStore 的时候,会接收一个默认的 reducer,如果后面想要重新换一个,就需要用到 replaceReducer 了

1
2
3
4
5
6
7
8
9
10
/**
* 替换 reducer
* @param {*} reducer
*/
const replaceReducer = (reducer) => {
// 直接把新的 reducer 覆盖掉旧的就行了
currentReducer = reducer;
// 替换之后派发一次 dispatch
dispatch({ type: 'MINI_REDUX_REPLACE' });
};

替换之后派发一次 dispatch 的目的是初始化一下新的 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
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 创建 store
* @param {*} reducer
* @param {*} initialState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initialState, enhancer) => {
let currentState; // 当前 state
let subQueue = []; // 创建一个监听队列
let currentReducer = reducer;
let isDispatch = false;

if (initialState) {
currentState = initialState;
}

/**
* 返回最新的 state
*/
const getState = () => {
return currentState;
};

/**
* 监听 state 的变动
* @param {*} listener 数据变化时要执行的函数
*/
const subscribe = (listener) => {
// 把监听函数放入监听队列里
subQueue.push(listener);
// 移除监听事件
return () => {
subQueue = subQueue.filter((l) => l !== listener);
};
};

/**
* 派发 action 并执行所有 监听函数
* @param {*} action
*/
const dispatch = (action) => {
// 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
if (isDispatch) {
throw new Error('dispatching');
}

try {
currentState = currentReducer(currentState, action);
isDispatch = true;
} finally {
isDispatch = false;
}

// 执行所有监听函数
subQueue.forEach((listener) => listener());
return action;
};

/**
* 替换 reducer
* @param {*} reducer
*/
const replaceReducer = (reducer) => {
if (reducer) {
// 直接把新的 reducer 覆盖掉旧的就行了
currentReducer = reducer;
}
// 替换之后派发一次 dispatch
dispatch({ type: 'MINI_REDUX_REPLACE' });
};

return {
getState,
dispatch,
subscribe,
replaceReducer,
};
};

export default createStore;

要想在项目中跑起来,光实现一个 createStore 是不够的

createStore 建造了一个仓库,还需要配送点(Provider)和送货员(connect)才能到用户(组件)手里

Provider、connect

首先,我们需要清楚他们三者之间的职责:

createStore:生成 store,返回一系列功能函数

Provider:把 createStore 返回的一系列函数传递到每个子组件里

connect:把 store 里的数据关联到组件上

Provider 的实现

Provider 的主要作用就是把 store 里的数据传递下去

1
2
3
4
5
6
7
8
9
10
11
// Provider.jsx
import React, { createContext } from 'react';

export const StoreContext = createContext(null);

const Provider = (props = {}) => {
const { store, children } = props;
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};

export default Provider;

connect 的实现

connect 是一个高阶组件,第二个参数为需要关联数据的组件,返回一个新组件

connect 的作用就是把 store 的数据关联到对应组件里,并监听 store 的变化,数据变化后更新对应组件

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
// connect.jsx
import React, { useContext, useEffect, useState } from 'react';
import { StoreContext } from './Provider';

const connect = (mapStateToProps, mapDispatchToProps) => (WrapComponent) => {
const ConnectComponent = () => {
const { getState, dispatch, subscribe } = useContext(StoreContext);
const [props, setProps] = useState({
getState,
dispatch,
});

let stateToProps;
let dispatchToProps;

const update = () => {
if (mapStateToProps) {
stateToProps = mapStateToProps(getState());
}

if (mapDispatchToProps) {
dispatchToProps = mapDispatchToProps(dispatch);
}

setProps({
...props,
...stateToProps,
...dispatchToProps,
});
};

useEffect(() => {
update();
subscribe(() => update());
}, []);

return <WrapComponent {...props} />;
};

return ConnectComponent;
};

export default connect;

总结

一个基础版的 mini redux 就实现完了,有空了把中间件相关的东西输出一下

这是我学习完相关内容之后输出的一个笔记,有写的不对的地方,还请各位铁汁指正 抱拳了老铁

体验在线 demo:点我点我点我

github:https://github.com/isxiaoxin/mini_redux