理解Redux

为什么需要 redux

就个人来说,偏前,开发过前端组件以及云开发控制台;偏后,使用 nodejs 开发过云开发的数据流中台以云开发网关,日流量达到 10 亿+的级别。

目前微服务理念非常火,后端架构都像无状态服务转变,这方便基于 k8s 的横向扩容,应对突发流量。

但是在前端开发中,尤其是控制台这种业务逻辑很重、交互细节繁多的场景下,都是基于数据状态来渲染视图。在 reactjs/vuejs 中,开发者只需要关注并且维护好数据状态即可。

那么问题来了,重交互的前端,怎么维护好这些细节呢?举个例子,对于一个 ajax 请求,它有 4 种状态:

  • 未发送
  • 发送中
  • 收到结果
    • 成功返回
    • 出错失败

那么根据这 4 种状态,就要展示不同的视图给用户。1 个 ajax 还好,但是现在 pc 端页面动则几十个异步请求,状态的维护变得日渐复杂。除了 ajax 的场景,表单、编辑器等,也是需要维护很多状态。

而 redux 的出现,就解决了前端的状态维护问题

什么是 redux

从上面可以看出来,redux 是状态管理工具。和直接修改状态相比,redux 有那些好处呢?

0、redux 不局限于 react

准确来说,redux 不局限于前端。它可以直接在 node 环境中运行,只是平时常说的 redux 基本都是 react-redux,相较于原生 redux,它在更新状态的时候,多了一步触发 react 视图刷新的操作。

由于现在服务都流行无状态化,后端一般也没有前端这么复杂细致的状态管理,所以后端并不常用 redux。但是为了方便说明,之后的演示都是直接在 nodejs 环境下。

1、redux 中,状态的变动是通过函数调用来触发的

如果直接写原生状态修改,当代码量变多,或者维护者变多的时候,不看代码无法知道哪些状态被修改了,并且只能通过拆解代码来确定状态更改的顺序。

1
2
3
4
5
6
7
8
9
const nestedInfo = {
people: {
location: {
country: "china",
},
},
};
// 修改状态
nestedInfo.people.location.country = "zh";

如果通过 redux,本质是通过函数调用来触发修改。所有的修改都要经过 redux,交由它来控制。配合一些 redux 中间件,可以在控制台输出变动过程,整个状态变动不再是黑盒子。

1
2
3
4
5
6
7
8
9
10
store.dispatch({
type: "CHANGE_PEOPLE",
payload: {
people: {
location: {
country: "china",
},
},
},
});

2、redux 可以处理异步

redux-think、redux-promise、redux-saga 都属于 redux 的中间件或者基于 redux 的异步解决方案。其中,redux-saga 基于 yield 协程,提供了更多的异步操作符。基于 redux-saga 再次封装的 saga-duck,借助 duck 编程思路,进一步统一了状态编程的写法,也是目前腾讯云控制台的标准写法。

这些中间件是基于 redux 的中间件机制,类似 koajs 的洋葱模型。通过挂入中间件,来实现更多功能。这个之后会详细提到。

基本概念和数据流

redux 由部分组成:

  • Store:全局唯一,是一个大对象,所有的状态都在其上更新
  • 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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* @Author: dongyuanxin
* @Date: 2020-12-24 23:24:33
* @Github: https://github.com/dongyuanxin/blog
* @Blog: https://0x98k.com/
* @Description: redux的简单用例
*/

const { createStore } = require("redux");

// 这就是一个Reducer,用来更新状态
function counter(state = 0, action) {
// state是上一次状态
// 根据action.type字段,来决定如何进行更新
switch (action.type) {
case "INCR":
console.log("[1] incr");
return state + 1;
case "DECR":
console.log("[1] decr");
return state - 1;
default:
// 一定要有默认返回上次状态,否则状态可能会丢失
return state;
}
}

// 全局唯一store,假设是个数字,初始值是1
// 注意:如果没有初始值,会在创建store的时候自动调用一次reducer,用来初始化状态
const store = createStore(counter, 1);

// 通过subscribe监听状态变化,这里直接打印最新状态
store.subscribe(() => {
console.log(">>> state is", store.getState());
});

// 调用reducer更新,使其+1
store.dispatch({ type: "INCR" });

整个过程是:通过diapatch函数,将action 下发到 reducer,reducer 根据 action.type 和 action.payload 来更新 store(action => reducer => store)。

参考