jiechenjiechen

The world is quiet here.

© jiechen

Rebuild in 2023   |   Start in 2021
Total View 0 Site Visitors 0

状态管理工具的演变

2022年5月27日front-end2539 字约 17 分钟

概述:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测

Store模式

将状态存到一个外部变量, this.$root.$data

1
2
3
4
5
6
7
8
9
10
11
12
var store = {
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
// 发生改变记录点日志啥的
this.state.message = newValue
},
clearMessageAction () {
this.state.message = ''
}
}

由于store的state值改变方式(mutation)只有通过触发action来操作,因此可以很容易跟踪到state的改变流程,出现错误也能通过日志明确错误位置

store并没有限制组件只能通过action来修改state
由这一点演化得出了 Flux 架构

Flux

Flux是类似于MVC、MVVM之类的一种思想,它把一个应用分成四个部分
——View、Action、Dispatcher、Store

View视图层可以是通过vue或者react等框架实现,而View中的数据都是Store,Store改变就抛出一个事件,通知所有的订阅者(或者监听,不同的框架对应不同的数据响应技术)发生改变

Flux要求,View要想修改Store,必须经过一套流程

  1. 视图先要告诉Dispatcher,让Dispatcher dispatch 一个 action
  2. Dispatcher 收到 View 发出的 action,然后转发给Store
  3. Store就触发相应的action来更新数据
  4. 数据更新则伴随着 View 的更新

注意:

  • Dispatcher的作用是接受所有的 Action。然后发给所有的 Store(Action可能是View触发的,也可能是其他地方触发的,如测试用例)
  • Store的改变只能通过Action,Store不应该有公开的 Setter,所有的Setter都应该是私有的,只能有公开的 Getter
  • 具体Action的处理逻辑一般放在 Store 里

Flux 特点: 单向流动

Redux

与 Flux 思想类似,
但修改了 Flux 的一些特性:

  • 一个应用可以拥有多个Store
  • 多个Store间可能存在依赖关系
  • Store 还封装了处理数据的逻辑

Store

Redux 里面只有一个 Store,整个应用的数据都在这个大 Store 里面。Store 的 State 不能直接修改,每次只能返回一个新的 State。Redux 整了一个 createStore 函数来生成 Store。

1
2
import { createStore } from 'redux';
const store = createStore(fn);

Store 允许使用 store.subscribe 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。这样不管 View 是用什么实现的,只要把 View 的更新函数 subscribe 一下,就可以实现 State 变化之后,View 自动渲染了。比如在 React 里,把组件的render方法或setState方法订阅进去就行。

Action

和 Flux 一样,Redux 里面也有 Action,Action 就是 View 发出的通知,告诉 Store State 要改变。Action 必须有一个 type 属性,代表 Action 的名称,其他可以设置一堆属性,作为参数供 State 变更时参考。

1
2
3
4
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};

Redux 可以用 Action Creator 批量来生成一些 Action。

Reducer

Redux 没有 Dispatcher 的概念,Store 里面已经集成了 dispatch 方法。store.dispatch()是 View 发出 Action 的唯一方法。

1
2
3
4
5
6
7
import { createStore } from 'redux';
const store = createStore(fn);

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

Redux 用一个叫做 Reducer 的纯函数来处理事件。Store 收到 Action 以后,必须给出一个新的 State(就是刚才说的Store 的 State 不能直接修改,每次只能返回一个新的 State),这样 View 才会发生变化。这种 State 的计算过程就叫做 Reduce

纯函数,即没有任何副作用

  • 对于相同的输入,永远都只会有相同发输出
  • 不会影响挖补的变量,也不会被外部变量影响
  • 不能改写参数

Redux根据应用的状态和当前的 action 推导出新的 state:(previousState, action) => newState

类比 Flux:(state, action) => state

1
2
3
question: 为什么叫做 Reducer 呢?
—— reduce 是一个函数式编程的概念,经常和 map 放在一起说,简单来说,map 就是映射,reduce 就是归纳。
映射就是把一个列表按照一定规则映射成另一个列表,而 reduce 是把一个列表通过一定规则进行合并,也可以理解为对初始值进行一系列的操作,返回一个新的值

整体流程

1、用户通过 View 发出 Action:

1
store.dispatch(action);

2、然后 Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

1
let nextState = xxxReducer(previousState, action);

3、State 一旦有变化,Store 就会调用监听函数。

1
store.subscribe(listener);

4、listener可以通过 store.getState() 得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。

1
2
3
4
function listerner() {
let newState = store.getState();
component.setState(newState);
}

对比 Flux

和 Flux 比较一下:Flux 中 Store 是各自为战的,每个 Store 只对对应的 View 负责,每次更新都只通知对应的View:

Redux 中各子 Reducer 都是由根 Reducer 统一管理的,每个子 Reducer 的变化都要经过根 Reducer 的整合:

简单来说,Redux有三大原则: 单一数据源:Flux 的数据源可以是多个。 State 是只读的:Flux 的 State 可以随便改。 * 使用纯函数来执行修改:Flux 执行修改的不一定是纯函数。

Redux 和 Flux 一样都是单向数据流

与React 关系

Redux 和 Flux 类似,只是一种思想或者规范,它和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

但是因为 React 包含函数式的思想,也是单向数据流,和 Redux 很搭,所以一般都用 Redux 来进行状态管理。为了简单处理 Redux 和 React UI 的绑定,一般通过一个叫 react-redux 的库和 React 配合使用,这个是 react 官方出的(如果不用 react-redux,那么手动处理 Redux 和 UI 的绑定,需要写很多重复的代码,很容易出错,而且有很多 UI 渲染逻辑的优化不一定能处理好)。

Redux将React组件分为容器型组件和展示型组件,容器型组件一般通过connect函数生成,它订阅了全局状态的变化,通过mapStateToProps函数,可以对全局状态进行过滤,而展示型组件不直接从global state获取数据,其数据来源于父组件。

如果一个组件既需要UI呈现,又需要业务逻辑处理,那就得拆,拆成一个容器组件包着一个展示组件。

Redux-saga

Redux处理异步操作,添加中间件后的产物

官方文档:Redux-Saga

Dva

官方定义:dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架

简单理解,就是让使用 react-redux 和 redux-saga 编写的代码组织起来更合理,维护起来更方便

之前我们聊了 redux、react-redux、redux-saga 之类的概念,大家肯定觉得头昏脑涨的,什么 action、reducer、saga 之类的,写一个功能要在这些js文件里面不停的切换。dva 做的事情很简单,就是让这些东西可以写到一起,不用分开来写了

比如:

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
// 以前书写的方式是创建 sagas/products.js, reducers/products.js 和actions/products.js,然后把 saga、action、reducer 啥的分开来写,来回切换

app.model({
// namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值
namespace: 'products',
// state - 对应 reducer 的 initialState
state: {
list: [],
loading: false,
},
// subscription - 在 dom ready 后执行
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
// effects - 对应 saga,并简化了使用
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
// reducers - 就是传统的 reducers
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});

MobX

官网:MobX 中文文档

对比 Flux 体系的单向数据流方案,Mobx 的思想则是 :任何源自应用状态的东西都应该自动地获得 ——状态只要一变,其他用到状态的地方就都跟着自动变

Flux 或者说 Redux 的思想主要就是函数式编程(FP)的思想,所以学习起来会觉得累一些。而 MobX 更接近于面向对象编程,它把 state 包装成可观察的对象,这个对象会驱动各种改变。什么是可观察?就是 MobX 老大哥在看着 state 呢。state 只要一改变,所有用到它的地方就都跟着改变了。这样整个 View 可以被 state 来驱动。

1
2
3
4
5
6
7
8
9
10
11
const obj = observable({
a: 1,
b: 2
})

autoRun(() => {
console.log(obj.a)
})

obj.b = 3 // 什么都没有发生
obj.a = 2 // observe 函数的回调触发了,控制台输出:2

上面的obj,他的 obj.a 属性被使用了,那么只要 obj.a 属性一变,所有使用的地方都会被调用。autoRun 就是这个老大哥,他看着所有依赖 obj.a 的地方,也就是收集所有对 obj.a 的依赖。当 obj.a 改变时,老大哥就会触发所有依赖去更新

MobX 和 Flux、Redux 一样,都是和具体的前端框架无关的,也就是说可以用于 React(mobx-react) 或者 Vue(mobx-vue)。一般来说,用到 React 比较常见,很少用于 Vue,因为 Vuex 本身就类似 MobX,很灵活。如果我们把 MobX 用于 React 或者 Vue,可以看到很多 setState() 和 this.state.xxx = 这样的处理都可以省了。

参考

Vuex、Flux、Redux、Redux-saga、Dva、MobX