Flux 数据流两三事儿

Flux是由 Facebook 在 2014 年 7 月提出的一种 React 应用体系架构,主要用于解决多层级组件之间数据传递以及状态管理的问题。并由此派生出了RefluxReduxMobX等一系列单向数据流框架。为 Web 前端页面实现组件化拆分之后,组件间的通信与协同机制提供了一套较为完善的方法学。其核心理念在于将所有应用状态放置在Store内进行统一管理,视图层组件只能通过触发Action修改Store中的应用状态。

本文首先系统的概括 Facebook 官方的Flux以及单向数据流思想,然后遵循近几年Flux 衍生框架的发展历程,逐步进行概括性的分析与比较,并顺带介绍了 Vue 技术栈当中的类 Flux 框架 VueX,最后,由于通常将Action视为 Flux 工作流的核心与起点,本文还对《Flux Standard Action》自述文档进行了翻译,以期更为全面的展现 Flux 生态的演进过程。

Flux

Flux 是 Facebook 官方构建 Web 前端应用体系架构,通过数据的单向流动有效补足了 React 组件间通信的短板,Flux 架构思想主要由如下 4 个部份组成:

  1. Action:视图层发出的动作信息,可以来自于用户交互,也可能来自于服务器响应。
  2. Dispatcher:派发器,用来接收 Actions 并执行相应回调函数。
  3. Store:用来存放应用的状态,一旦发生变化就会通知视图进行重绘。
  4. View:React 组件视图。

单向数据流

所谓的单向数据流(unidirectional data flow是指用户访问ViewView发出用户交互的ActionDispatcher收到Action之后,要求Store进行相应更新。Store更新后会发出一个change事件,View收到change事件后更新页面的过程。这样数据总是清晰的单向进行流动,便于维护并且可以预测。

Dispatcher

dispatcher集中管理 Flux 应用程序的全部数据流,本质上是store上注册的回调函数,主要用于分发actionstore,并维护多个store之间的依赖关系(官方实现是通过 Dispatcher 类上的 waitFor()方法)。

Action Creator发起一个新的actiondispatcher,应用中的所有store都将通过注册的回调函数接收到action。伴随应用程序的增长,dispatcher会变得极为重要,因为需要它通过指定顺序的回调函数去管理store之间的依赖。Store会声明式的等待其它store完成更新后再相应的更新自己。

dispatcher是官方 Flux 当中actionstore的粘合剂。

Store

store包含应用的state和逻辑,作用类似于传统 MVC 中的Model,但它管理着多个对象的状态。

store将自己注册到dispatcher,并提供一个接收action作为参数的回调函数。在store注册的回调函数中,将会通过基于action类型的switch判断语句进行解释操作,并为store的内部方法提供适当的钩子函数。这允许action通过dispatcherstore当中的state进行更新。在这些store被更新之后,会广播一个事件声明其状态已经被改变,从而让view可以设置新的state并更新自己。

View

在 React 嵌套视图层级结构的顶部,有一种特殊的视图可以监听其所依赖的store广播的事件,这种视图称为控制器视图controller-view)。因为它提供了从store获取数据的粘合代码,并传递这些数据给子组件。

当控制器视图接收到来自store的事件时,会首先通过store公有的getter()方法获取新数据,然后调用组件自身的setState()forceUpdate()方法,使其本身以及子组件的render()方法都得到调用。

通常会将store上的全部state传递给单个对象的视图链,允许不同的子组件按需进行使用。除了将控制器的行为保持在视图层次结构的顶部,从而让子视图尽可能地保持功能上的纯洁外;将存储在单个对象中的整个状态传递下来,也可以减少我们需要管理的props的数量。

有些时候,可能需要向更深的视图层次结构添加额外的控制器视图以保持组件的简单,这可能有助于更好的封装与指定数据域相关的那部分视图层次结构。然而值得注意的是,这些更深层次的控制器视图会引入新的数据流入口,从而破坏单向数据流并引发各种奇怪的错误,而且难以 debug。因此,在决定添加更深层次控制器视图之前,需要仔细的进行权衡。

Action

dispatcher会暴露一个接收action的方法,去触发store的调度,并包含数据的payload[ˈpeɪləʊd] n.有效载荷)。action的建立可能会被封装到用于发送actiondispatcher的语义化的帮助函数(Action Creator)当中。例如:我们需要改变to-do-list应用中的 1 个to-do-item,就需要在TodoActions模块中建立签名为updateText(todoId, newText)的函数,该函数能被视图组件里的事件处理器调用以响应用户交互。这个 Action Creator 方法还需要添加一个type属性到action,这样当actionstore中被解释的时候可以被正确的响应。前面例子中,type属性的值可以是TODO_UPDATE_TEXT

action也可能来自其它地方,比如服务器。在组件数据初始化的过程中,或者服务器返回错误代码,以及服务器存在需要提供给应用程序的更新的时候。

Reflux

Reflux是一种 Flux 单向数据流体系架构的具体实现,主要由actionstore组成,其中action会在重新回到视图组件之前初始化新的数据并传递到store,而视图组件只能通过发送action去改变store中的数据。

相当长一段时间里,开源社区普遍认为官方 Flux 又臭又长过于学院派,因此 Reflux 实现大幅精简 Flux 的各类晦涩概念(最大的变化是移除了dispatcher),只保留如下 3 个主要概念:

  1. 建立 Action。
  2. 建立 Store。
  3. 连接 React 组件和 Store。

存在炫技的倾向,将简单概念复杂化解读是开发人员编写技术文档一个通病

建立 Action

调用Reflux.createAction()并传入可选的配置对象就能新建一个 Action,这个配置对象的可选项如下:

1
2
3
4
5
6
7
8
{
actionName: 'myActionName', // action的名称
children: ['childAction'], // 异步操作中由子操作action名称所组成的数组
asyncResult: true, // 设置为true会快捷添加'completed'和'failed'两个子action
sync: false, // 设置action同步或者异步的发生(默认是同步的,除非存在子action)
preEmit: function(){...}, // 定义preEmit方法
shouldEmit: function(){...} // 定义shouldEmit方法
}

当然,建立 Action 时也可以缺省传入配置对象,如同下面代码中这样:

1
2
3
let statusUpdate = Reflux.createAction();

statusUpdate(data); // 调用Action

Reflux 中 Action 是一个能够被其它函数所调用的普通函数式对象。

还可以调用Reflux.createActions([...])一次性建立多个action

1
2
3
4
5
6
7
8
9
// 现在Actions对象拥有了多个action
var Actions = Reflux.createActions([
"statusAdded"
"statusEdited",
"statusRemoved",
]);

// 通过Actions.xxx的方式调用指定的Action
Actions.statusRemoved();

Reflux 中的 Action 还可以使用子 Action 异步加载文件,进行preEmitshouldEmit检查(Action 发出事件之前被调用),并拥有多个易于使用的快捷方式。

异步 Action 处理

正如上面所描述的,一个 Action 可以简单的通过myAction()方式进行调用,如果sync属性被设置为true则 Action 是同步的,将会立刻通过myAction.trigger()被执行;如是sync设置为false则是异步 Action,将会在 JavaScript 事件循环的下一个 tick 内通过myAction.triggerAsync()执行,并且 Action 配置对象的children属性可能会被设置。

1
2
3
4
5
6
let Actions = Reflux.createActions([
{
actionName: "myName",
sync: false,
},
]);

Action 默认情况下是同步的,除非在配置对象内进行了其它配置,或者 Action 本身还包含了其它子 Action。

当需要通过子 Action 去执行诸如文件加载一类的的异步 Action,那么该 Action 需要监听自身然后去执行这个异步操作,当操作完成的时候调用其子 Action,下面代码简单体现了这一过程:

1
2
3
4
5
let action = Reflux.createAction({ children: ["delayComplete"] });

action.listen(function () {
setTimeout(this.delayComplete, 50000);
});

建立 Store

Flux 建立store的方式非常类似于 React 组件,通过继承Reflux.Store对象就可以得到一个store,该store和组件一样都拥有一个可以通过setState()方法进行更新的state属性。

可以在store对象的constructor()方法内设置state的初值,并使用listenTo()设置指定action的监听器。

1
2
3
4
5
6
7
8
9
10
11
12
class StatusStore extends Reflux.Store {
constructor() {
super();
this.state = { flag: "OFF" }; // 设置store的默认state
this.listenTo(statusUpdate, this.onStatusUpdate); // 监听statusUpdate action并使用onStatusUpdate函数进行响应
}

onStatusUpdate(status) {
let newFlag = status ? "ON" : "OFF";
this.setState({ flag: newFlag });
}
}

上面例子中名为statusUpdate的 Action 被调用后,store上的onStatusUpdate()回调函数会传入 Action 上携带的参数。例如:Action 以statusUpdate(true)方式调用,那么onStatusUpdate()函数中的status参数值就为true

Store 可以通过this.listenables()方便的整合多个 Action,当一个 Action 对象或者一个 Action 对象的数组应用到该函数上,Reflux 可以根据命名约定自动添加监听器。只需要在action名称之后重命名这些函数的名称,例如自动在actionName前加上on,使之变成 Store 中action事件回调函数的名称onActionName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let Actions = Reflux.createActions(["firstAction", "secondAction"]);

class StatusStore extends Reflux.Store {
constructor() {
super();
this.listenables = Actions;
}

onFirstAction() {
// 被Actions.firstAction()触发
}

onSecondAction() {
// 被Actions.secondAction()触发
}
}

Flux 中的 Store 非常强大,甚至可以贡献出一个全局状态(正如 Redux 所倡导的那样),可以只对部分状态进行读取或设置,或是对全部状态进行时间旅行、调试。

连接 React 组件和 Store

建立 Action 和 Store 之后,最后一步就是将 Store 与 React 组件连接起来。

Reflux 中通过Reflux.Component新建 React 组件,而不是继续使用React.Component。由于Reflux.Component底层实现上继承了React.Component,因此两者功能和特性完全相同,唯一区别在于通过继承Reflux.Component实现的组件能够设置store并且从中获取state

1
2
3
4
5
6
7
8
9
10
11
12
class MyComponent extends Reflux.Component {
constructor(props) {
super(props);
this.state = {}; // our store will add its own state to the component's
this.store = StatusStore; // 将上一步建立的StatusStore传递给当前组件
}

render() {
let flag = this.state.flag; // flag属性已经从StatusStore中混入(mixin)当前组件的state
return <div>开关状态:{flag}</div>;
}
}

当组件挂载之后,将会建立一个新的StatusStore的单例对象(如果没有),或是使用一个已经建立的单例对象(由使用这个 Store 的其它组件建立)。但是,这里还可以注意如下 2 点:

  1. 可以向this.stores传递一个store数组来方便的设置多个 Store。
  2. 设置一个this.storeKeys数组来约束store上的指定部分会被混入(mixin)到组件的state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyComponent extends Reflux.Component {
constructor(props) {
super(props);
this.state = { type: "admin" }; // 注意我们仍然在使用普通的state
this.stores = [StatusStore, AnotherStore];
this.storeKeys = ["flag", "info"];
}

render() {
var flag = this.state.flag;
var info = this.state.info;
var type = this.state.type;
return (
<div>
Flag {flag}, Info: {info}, Type: {type}。
</div>
);
}
}

上面的例子中,将会混入StatusStoreAnotherStore中的state,但是又由于this.storeKeys只允许混入flaginfo,因此其它属性(例如 type 属性)不会被混入,而type在组件的构造方法内已经进行了赋值处理,否则render()函数渲染时会因为获取不到type属性的值而报错。

Reflux 以简单直接的方式整合store到组件上,可以将所有store聚合到一起,然后让组件有选择性的按需进行过滤。

虽然截止笔者成文为止,Reflux 的最后一次提交记录还停留在一年前的 2017 年 2 月,但是个人认为针对于中小型项目,Reflux 相对后续发展起来的 Redux 更加简单明了一些。

Redux

Redux可以认为是 Flux 思想的一种实现,两者在存在许多相同点的同时,也有诸多方面的异同。

Flux 和 Redux 都规定,将模型的更新逻辑放置在特定的逻辑层(Flux 里是store,Redux 是reducer)。Flux 和 Redux 都不允许直接修改store,而是使用称为action的普通 JavaScript 对象来对更改进行描述。两者不同之处在于,Redux 并没有dispatcher概念,通过使用纯函数去代替 Flux 中的事件处理器。

Redux 实质上在官方 Flux 基础上增加了诸多细节,因此各类概念又臭又长的问题依然未有任何实质性改观(如上图),其提出的概念要比其实现的代码略多 ⊙﹏⊙‖。

基本原则

  1. 单一数据源,整个应用的state被储存在一棵对象树,并且这棵对象树只存在于唯一的store当中。
  2. State 是只读的,修改state的唯一方法是触发actionaction本质是一个用于描述发生事件的普通 JavaScript 对象。
  3. 使用纯函数reducer来执行修改,从而描述action如何修改state树。

Reducer只是一些纯函数,它接收先前的stateaction,并返回新的state。刚开始你可以只有一个reducer,随着应用变大,你可以把它拆成多个小的reducers,分别独立地操作state树的不同部分。

Action

Action 是把数据从应用(视图交互或服务器响应数据)传递到store的有效载荷,是store数据的唯一来源,通常需要通过store.dispatch()action传递至store

Action 本质上是一个 JavaScript 普通对象,通常约定action内必须使用一个字符串类型的type字段来表示将要执行的动作。type可以被简单的定义为字符串常量,应用规模较大的时候建议使用单独模块存放action。除type字段外,action对象的结构完全由开发人员自行决定。当然也可以参照 Redux 社区制定的《Flux 标准 Action 规范》

1
2
3
4
5
6
{
type: 'ADD_TODO',
payload: new Error(),
error: true,
meta:{},
}

Action 创建函数即 Flux 中的 Action Creator)是生成action的方法,Redux 中的Action 创建函数只是简单返回一个action

1
2
3
4
5
6
7
8
function addTodo(text) {
return {
type: "ADD_TODO",
payload: new Error(),
error: true,
meta: {},
};
}

前面介绍的官方 Flux 实现当中,调用Action 创建函数将会触发dispatch,参考下面的代码:

1
2
3
4
5
6
7
8
function addTodoWithFlux(text) {
const action = {
type: ADD_TODO,
text,
};
// Flux官方实现将dispatch()方法放置在action创建函数当中
dispatch(action);
}

Redux 当中,只需要将Action 创建函数的结果传递给dispatch()即可发起一次dispatch过程。

1
2
// 将Redux创建函数直接传递给dispatch()
dispatch(addTodo(text));

虽然 Redux 的store里能直接调用store.dispatch(),但是多数情况下会使用react-redux提供的connect()来进行调用。bindActionCreators()可以自动将多个 Action 创建函数绑定至dispatch()

Reducer

Reducer 用来描述如何根据actionstore进行修改,是actionstore的黏合剂(官方 Flux 中充当这一作用的是 dispatcher)。

Reducer 本质是一个纯函数,接收旧的stateaction,返回新的state

1
(previousState, action) => newState;

一定要保持 Reducer 的纯净,只要传入参数相同,Reducer 返回的下一个state就一定相同(没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算)。

下面的代码声明了todostodosFilter两个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
import { combineReducers } from "redux";

// todos reducer
function todos(state = [], action) {
// 判断action的类型,分别返回不同的状态
switch (action.type) {
case ADD_TODO:
return [
...state,
// 合并到状态中的内容
{ text: action.text, completed: false },
];
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed,
});
}
return todo;
});
default:
return state;
}
}

// visibilityFilter reducer
function todosFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}

每个reducer只负责管理全局state中的一部分,其state参数只对应它管理的那部分state数据。

1
2
3
4
5
6
7
// 合并Reducer,是上面代码的语法糖
export default function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
todosFilter: todosFilter(state.todosFilter, action),
};
}

伴随应用规模的膨胀,可能需要将reducer拆分到不同的文件, 以保持其独立性并专门用于处理不同数据域。因此 Redux 提供了combineReducers()工具方法来完成上面todoApp的工作,从而实现简化代码的目的。

1
2
3
4
5
// 合并Reducer,是上面代码的语法糖
export default todoApp = combineReducers({
todos,
todosFilter,
});

combineReducers()用来生成一个调用一系列自定义reducer的函数,每个reducer根据key筛选出state中的具体一部分数据并处理,最后这个生成函数会将所有reducer的返回结果合并为一个大对象。

Store

Action用来描述发生的行为,Reducer用来根据action更新stateStore则是两者联系的关键,Redux 应用只有一个单一的store,当需要拆分数据处理逻辑时,应该组合使用reducer,而非创建多个store

  • 保存应用的state
  • 提供getState()方法获取state
  • 提供dispatch(action)方法更新state
  • 通过subscribe(listener)注册监听器;
  • 使用subscribe(listener)返回的函数注销监听器。
1
2
3
4
import { createStore } from "redux";
import todoApp from "./reducers";

let store = createStore(todoApp);

Middleware 与 Thunk

Redux 的Middleware可以提供action发起之后,到达reducer之前的扩展点,因此可以利用Middleware进行日志记录、创建崩溃报告、调用异步接口或路由等。

通过使用指定的Middlewareredux-thunkredux-promiseredux-rx),Action 创建函数Action Creator)除了返回action对象外还可以返回函数PromiseObservable,这种情况下 Action 创建函数就被称为thunk([θʌŋk] 形实转换程序)。

Action 创建函数返回的函数会被redux-thunk中间件执行,该函数不需要保持纯净,可以执行异步请求或者dispatch一个或多个action

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
import fetch from "isomorphic-fetch";

export const REQUEST_INFO = "REQUEST_INFO";
function requestInfo(info) {
return {
type: REQUEST_INFO,
info,
};
}

export const RECEIVE_INFO = "RECEIVE_INFO";
function receiveInfo(info, json) {
return {
type: RECEIVE_INFO,
info,
};
}

// Thunk函数的使用与普通Action创建函数相同:store.dispatch(fetchInfo('Hello Hank!'))
export function fetchInfo(info) {
// 将dispatch方法通过参数传递给返回的函数,使返回函数体内也具备dispatch action的能力
return function (dispatch) {
dispatch(requestInfo(info)); // 首次dispatch更新state去通知API请求发起
// thunk函数可以有返回值,该返回值会作为dispatch方法的返回值传递,下面的代码返回一个promise
return fetch(`http://www.uinika.cn/test/${info}.json`)
.then((response) => response.json())
.then(
(json) => dispatch(receivePosts(info, json)) // 使用请求结果更新应用的state
);
};
}

远程 API 请求通常需要发起 3 种异步 Action,分别用于通知reducer请求开始、成功、失败,可以考虑向action添加一个status字段来区分 3 种状态。

Redux 提供的createStore()创建的store只支持同步数据流,需要通过 Redux 的applyMiddleware()方法应用redux-thunk去支持异步数据流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createStore, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import { fetchInfo } from "./actions";
import rootReducer from "./reducers";

const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
)
);

store.dispatch(
fetchInfo("Hello Hank!").then(
() => console.info(store.getState()
)
);

连接 Redux 与 React

React 编写的 App 组件需要连接至 Redux,使其能够dispatch actions以及从Redux store读取state

首先,需要通过react-redux提供的<Provider>包裹应用的根组件,使得store可以被根组件下的所有子级组件访问(通过 React 的 context 特性实现)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./components/App";
import todoApp from "./reducers";

let store = createStore(todoApp);

let root = document.getElementById("root");

render(
<Provider store={store}>
<App />
</Provider>,
root
);

然后,通过react-redux提供的connect()方法将包装好的组件连接至 Redux。尽量只做一个顶层的组件,或者route处理。

connect()包装的组件可以得到一个dispatch()作为组件的props,以及获取全局state中所需的任意内容。connect()方法的唯一参数是selector,该方法从store接收到全局的state,然后返回组件中需要的 props

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
import {
addTodo,
completeTodo,
setVisibilityFilter,
VisibilityFilters,
} from "../actions";
import React, { Component, PropTypes } from "react";
import TodoList from "../components/TodoList";
import AddTodo from "../components/AddTodo";
import Footer from "../components/Footer";
import { connect } from "react-redux";

class App extends Component {
render() {
const { dispatch, visibleTodos, visibilityFilter } = this.props; // 通过调用connect()进行注入
return (
<div>
<AddTodo onAddClick={(text) => dispatch(addTodo(text))} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={(index) => dispatch(completeTodo(index))}
/>
<Footer
filter={visibilityFilter}
onFilterChange={(nextFilter) =>
dispatch(setVisibilityFilter(nextFilter))
}
/>
</div>
);
}
}

function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter((todo) => !todo.completed);
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter((todo) => todo.completed);
}
}

// 选择需要哪部分state注入至组件的props
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter,
};
}

// 注入dispatch和state到App组件
export default connect(select)(App);

Mobx

Mobx 是最新的 React 状态管理解决方案,其设计思想大量借鉴自 Vuex,能够方便的对状态进行自动更新,并且提供了十分好用的装饰器语法糖。

首先,通过下面语句,安装 Mobx 及其绑定库、装饰器语法支持。

1
2
3
➜  npm install mobx --save
➜ npm install mobx-react --save
➜ npm install --save-dev babel-preset-mobx

然后,修改.babelrc打开装饰器语法支持:

1
2
3
{
"presets": ["mobx"]
}

如果使用 VSCode 编辑器,启用 Mobx 装饰器语法支持后,需要添加如下设置项,开启语法支持避免编辑器出现错误提示。

1
2
3
{
"javascript.implicitProjectConfig.experimentalDecorators": true
}

基本概念

  • State状态,驱动 Mobx 应用程序的数据。
  • Action动作,一段可以改变状态 State 的代码,例如:用户事件、后端推送等。严格模式下,MobX 强制只能使用 Action 修改 State。
  • Derivation[deri'veiʃən] 推导,衍生,源自状态并且不会再有进一步的相互作用,MobX 将衍生分为如下两种类型:
    1. Reaction反应,指 State 状态发生改变时自动产生的变化,在 MobX 当中较为常用。
    2. Computed values计算值,通过纯函数监听可观察的 State,并根据这些 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
34
35
36
37
38
39
40
41
42
import { observable, autorun } from "mobx";

class Store {
@observable testNumber = 100;
@observable testString = "test string!";
@observable testObject = { a: 1, b: 2 };
@observable testArray = [1, 2, 3, 4, 5];
}

const storeInstance = new Store();

// autorun()函数首先会触发一次,然后每次observe数据发生变化时会再次触发
autorun(() => {
console.log("--------");
console.info(storeInstance.testNumber);
console.info(storeInstance.testString);
console.dir(storeInstance.testObject);
console.dir(storeInstance.testArray);
});

storeInstance.testNumber = 200;
storeInstance.testString = "another test string!";
storeInstance.testObject.b = 0;
storeInstance.testArray[0] = 6;

/** 输出结果:
--------
100
test string!
Object
ObservableArray
--------
200
test string!
Object
ObservableArray
--------
200
another test string!
Object
ObservableArray
*/

结合 React 使用

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
import { observable } from "mobx";
import { observer } from "mobx-react";

// 建立store并使time属性处于observable状态
class Store {
@observable time = 0;
}

const storeInstance = new Store();

// 每间隔1秒time属性就加一
setInterval(() => {
storeInstance.time++;
}, 1000);

// 使用@observer声明Timer组件,当storeInstance当中的time属性发生变化时,该组件将会被实时渲染
@observer
class Timer extends React.Component {
render() {
return <h1>传入时间: {this.props.store.time} </h1>;
}
}

// 将storeInstance作为Timer组件的props传入,并将实时结果渲染至DOM
export default class Dashboard extends React.Component {
render() {
return (
<div id="dashboard" className="animated fadeIn">
<Timer store={storeInstance} />
</div>
);
}
}

计算值 computed

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
import { observable, computed } from "mobx";
import { observer } from "mobx-react";

class Store {
@observable price = 100;
@observable amount = 200;
// 在类属性的getter上通过@computed装饰器创建计算属性
@computed get total() {
return this.price * this.amount;
}
}

const storeOrder = new Store();

setInterval(() => {
// 随机更新observable的属性值,然后观察计算属性total的变化
storeOrder.price += Math.random();
}, 1500);

@observer
class Counter extends React.Component {
render() {
return <h1>实时价格: {this.props.store.total} </h1>;
}
}

export default class Dashboard extends React.Component {
render() {
return (
<div id="dashboard" className="animated fadeIn">
<Counter store={storeOrder} />
</div>
);
}
}

Mobx 相对于 Redux,最大的进步在于将 Redux 当中繁琐的 Reducer 声明进行了简化,通过类似于 VueX 的显式双向绑定方式,实时将需要更新的值反映至相应组件,是一款现代化 Flux 框架的正确打开方式。

Vuex

Vuex 是由 Vue 前端生态圈提出的一种应用状态管理库,能够与 Vue 无缝进行集成。如果关闭其严格模式,则不需要再书写繁琐的 Mutation(类似于 Redux 中 Reducer 的作用),而直接将 Vuex 的store与 Vue 组件的data进行响应式绑定,这个对于单张页面内多组件间存在大量交互数据的场景非常有用。

1
2
3
4
const appStore = new Vuex.Store({
// ...
strict: true,
});

State

和 React 生态圈下的 Flux 解决方案一样,Vuex 同样通过一个状态对象管理全部的应用状态,换而言之,每个应用将仅包含一个Store实例。

1
2
3
4
5
6
7
8
const store = new Vuex.Store({
state: {
users: [
{ username: "hank", password: "123" },
{ username: "uinika", password: "456" },
],
},
});

Getter

Vuex 允许在Store定义getter,其作用类似于前面提到的 Mobx 的@computed计算属性。getter返回值会根据其所依赖的状态进行缓存,只有该依赖状态发生了变化的情况下才会重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
state: {
users: [
{ username: "hank", password: "123", isLogin: true },
{ username: "uinika", password: "456", isLogin: false },
],
},
getters: {
doneTodos: (state) => {
return state.isLogin.filter((user) => user.isLogin);
},
},
});

Mutation

开启严格模式的情况下,Vuex 更改Store中的状态的唯一方法是提交mutation,其作用和用法类似于 Redux 中的reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
state: {
user: { username: "hank", password: "123", isLogin: false },
},
mutations: {
login(state) {
// 变更状态
state.user.isLogin = true;
},
},
});

store.commit("login"); // 提交Mutation

Action

类 Flux 框架,总是少不了 Action 的存在,Vuex 中actionmutation的不同之处在于:action只能提交mutation,而不能直接对store进行修改,副作用的操作(例如异步的请求)可以书写在action当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const store = new Vuex.Store({
state: {
user: { username: "hank", password: "123", isLogin: false },
},
mutations: {
login(state) {
// 变更状态
state.user.isLogin = true;
},
},
actions: {
login(context) {
context.commit("login");
},
},
});

store.commit("login"); // 提交并触发Mutation
store.dispatch("login"); // 提交并触发Action

Module

在应用较为复杂的场景下,单一的Store对象可能变得非常臃肿,因此有必要通过模块化进行更细粒度的划分。Vuex 提出的模块化Store为此提供了非常良好的体验,允许将Store分割为模块(module),每个模块拥有自己的statemutationactiongetter甚至嵌套的子模块。

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
const module1 = {
state: {
/* ... */
},
mutations: {
/* ... */
},
actions: {
/* ... */
},
getters: {
/* ... */
},
};

const module2 = {
state: {
/* ... */
},
mutations: {
/* ... */
},
actions: {
/* ... */
},
};

const store = new Vuex.Store({
modules: {
a: module1,
b: module2,
},
});

store.state.a; // module1的状态
store.state.b; // module2的状态

结构良好的 Vuex 项目

添加 Vuex 热重载与模块化支持,并将嵌套的子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
import Vue from "vue";
import Vuex from "vuex";
import Demo from "./demo/script.store";

Vue.use(Vuex);

const Store = new Vuex.Store({
strict: process.env.NODE_ENV !== "production",
modules: {
Demo, // 声明模块
},
});

if (module.hot) {
module.hot.accept(["./store", "./demo/script.store"], () => {
Store.hotUpdate({
modules: {
Demo: require("./demo/script.store").default,
},
});
console.info("Vue hot update!");
});
}

const app = new Vue({
Store,
}).$mount("#app");

嵌套的子Store中需要使用namespaced: true,开启模块命名空间,所有getteractionmutation都会根据模块注册的路径调整命名。

1
2
3
4
5
6
7
8
export default {
namespaced: true,
state: {
data1: {},
data2: [],
loading: false, // 加载动画
},
};

除此之外,Vuex 提供的一系列 mapper 语法糖能够便捷的融合 Vuex 的各类特性,有着良好的书写体验与开发效率。整体来看,Vuex 是过去诸多 Flux 框架实现的集大成者,对 React 生态下 Mobx 的开发有着非常深刻的影响,堪称现代化 Flux 框架实现的典范

FSA

Redux 社区制订了《Flux Standard Action》规范,这是一套人机友好并且较为通用的Action对象定义规范,下面代码是一个满足FSA标准的 Action 对象。

1
2
3
4
5
6
{
type: 'UPDATE',
payload: new Error(),
error: true
meta: {}
}

诸多 Flux 实现在处理异步队列时,往往会增加FETCH_SUCCESSFETCH_FAILURE两个 Action 类型,这样的方式其实并不理想,因为它重载了 2 个独立的关注点:标识 action 是否需要表达错误、从全局 action 队列中消除某一类型 Action 的歧义,而在 FSA 当中会将error视为头等概念。

FSA 标准主要为了达成如下 3 个设计目标:

  1. 简单:对象结构简单、直接、灵活。
  2. 人性化:便于开发人员编写和阅读。
  3. 有效:Action 该能够创建有用的工具和进行抽象。

type

actiontype属性用于指明发生动作的性质,通常是一个字符串常量。如果两种类型是相同的,那么其type属性必须(通过===)严格等价。

payload

可选的payload属性用于表示动作的有效载荷,可以是任何类型的值。任何非表达类型或状态的 Action 相关信息,都应该是payload的一部分。如果error属性为true,那么payload属性应该是一个错误对象,类似于拒绝一个带有错误对象的 Promise。

error

可选的error属性用于在action出现错误的时候被设置为true,主要起到一个错误标志位的作用。因为根据上面的约定,错误对象本身需要放置到payload属性上。如果error属性拥有除true之外的其它值(undefinednull),则action并不能被解析为错误。

meta

可选的meta元属性可以是任何类型的值,主要用于放置一些额外的信息,并非有效载荷payload的一部分。

FSA 提供的工具函数

FSA 提供了flux-standard-action项目,可以通过下面命令安装:

1
npm i flux-standard-action --save

该 npm 包提供了如下 2 个辅助函数:

  • isFSA(action):判断action是否满足 FSA 标准。
  • isError(action)action出现错误的时候返回true

redux-actionsredux-promiseredux-rx等第三方库都遵循了 FSA 规范。