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,这个配置对象的可选项如下:

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

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

let statusUpdate = Reflux.createAction();

statusUpdate(data); // 调用Action

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

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

// 现在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 属性可能会被设置。

let Actions = Reflux.createActions([
  {
    actionName: "myName",
    sync: false,
  },
]);

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

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

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 的监听器。

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()

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

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
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 规范》

{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true,
  meta:{},
}

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

function addTodo(text) {
  return {
    type: "ADD_TODO",
    payload: new Error(),
    error: true,
    meta: {},
  };
}

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

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

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

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

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

Reducer

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

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

(previousState, action) => newState;

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

下面的代码声明了 todostodosFilter 两个 reducer

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 数据。

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

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

// 合并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) 返回的函数注销监听器。
import { createStore } from "redux";
import todoApp from "./reducers";

let store = createStore(todoApp);

Middleware 与 Thunk

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

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

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

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 去支持异步数据流。

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 特性实现 )。

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

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 及其绑定库、装饰器语法支持。

➜  npm install mobx --save
➜  npm install mobx-react --save
➜  npm install --save-dev babel-preset-mobx

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

{
  "presets": ["mobx"]
}

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

{
  "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 的变化重新来计算新值。

简单例子

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 使用

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

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 进行响应式绑定,这个对于单张页面内多组件间存在大量交互数据的场景非常有用。

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

State

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

const store = new Vuex.Store({
  state: {
    users: [
      { username: "hank", password: "123" },
      { username: "uinika", password: "456" },
    ],
  },
});

Getter

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

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

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 当中。

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 甚至嵌套的子模块。

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 解耦至子组件的代码目录,便于查看与管理。

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 都会根据模块注册的路径调整命名。

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 对象。

{
  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 项目,可以通过下面命令安装:

npm i flux-standard-action --save

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

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

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章