前端状态管理与有限状态机

8,331 阅读11分钟

当下前端流行的框架,都是用状态来描述界面(state => view),可以说前端开发实际上就是在维护各种状态(state),这已经成为目前前端开发的共识。

View = ViewModel(Model);

理想情况下,ViewModel 是纯函数,给定相同的 Model,产出相同的 View。

state => view 很好理解,但如何在 view 中合理地修改 state 也是一个问题。

为什么需要状态管理

举个例子

图书馆的管理,原来是开放式的,所有人可以随意进出书库借书还书,如果人数不多,这种方式可以减少流程,增加效率,一旦人数变多就势必造成混乱。

Flux 就像是给这个图书馆加上了一个管理员,所有借书还书的行为都需要委托管理员去做,管理员会规范对书库的操作行为,也会记录每个人的操作,减少混乱的现象。

一个比喻

我们寄一件东西的过程

没有快递时:

  • 打包准备好要送出去的东西
  • 直接到朋友家,把东西送给朋友
  • 很直接很方便,很费时间

有了快递公司:

  • 打包准备好要送出去的东西
  • 到快递公司,填写物品,收件人等基本信息
  • 快递公司替你送物品到你的朋友家,我们的工作结束了

多了快递公司,让快递公司给我们送快递。

当我们只寄送物品给一个朋友,次数较少,物品又较少的时候,我们直接去朋友家就挺好的。但当我们要频繁寄送给很多朋友很多商品的时候,问题就复杂了。

软件工程的本质即是管理复杂度。使用状态管理类框架会有一定的学习成本而且通常会把简单的事情做复杂,但如果我们想做复杂一点的事情(同时寄很多物品到多个不同地址),对我们来说,快递会让复杂的事情变的简单。

这同时也解释了,是否需要添加状态管理框架,我们可以根据自己的业务实际情况和技术团队的偏好而添加,有些情况下,创建一个全局对象就能解决很多问题。

核心思想

Flux 的核心思想:数据单向流动。

  • 不同组件的 state,存放在一个外部的、公共的 Store 上面。
  • 组件订阅 Store 的不同部分。
  • 组件发送(dispatch)动作(action),引发 Store 的更新。

Redux 的核心概念

  • 所有的状态存放在 Store。组件每次重新渲染,都必须由状态变化引起。
  • 用户在 UI 上发出 action。
  • reducer 函数接收 action,然后根据当前的 state,计算出新的 state。

Redux store 是单一数据源。Redux 没有 dispatcher 的概念,转而使用纯函数(pure function)代替。

Redux store 是不可变的(Immutable)。

MobX

  • Observable:它的 state 是可被观察的,无论是基本数据类型还是引用数据类型,都可以使用 MobX 的 (@)observable 来转变为 observable value。
  • Reactions:它包含不同的概念,基于被观察数据的更新导致某个计算值(computed values),或者是发送网络请求以及更新视图等,都属于响应的范畴,这也是响应式编程(Reactive Programming)在 JavaScript 中的一个应用。
  • Actions:它相当于所有响应的源头,例如用户在视图上的操作,或是某个网络请求的响应导致的被观察数据的变更。

和 Redux 对单向数据流的严格规范不同,Mobx 只专注于从 store 到 view 的过程。在 Redux 中,数据的变更需要监听,而 Mobx 的数据依赖是基于运行时的,这点和 Vuex 更为接近。

Vuex 的状态管理模式

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

Flux

Facebook 提出了 Flux 架构思想,规范了数据在应用中的流动方式。其基本架构如下入所示,其核心理念是单向数据流,它完善了 React 对应用状态的管理。

上图描述了页面的启动和运行原理:

1.通过 dispatcher 派发 action,并利用 store 中的 action 处理逻辑更新状态和 view

2.而 view 也可以触发新的 action,从而进入新的步骤 1

其中的 action 是用于描述动作的简单对象,通常通过用户对 view 的操作产生,包括动作类型和动作所携带的所需参数,比如描述删除列表项的 action:

{
    type: types.DELETE_ITEM,
    id: id
};

而 dispatcher 用于对 action 进行分发,分发的目标就是注册在 store 里的事件处理函数:

dispatcher.register(function(action) {
  switch (action.type) {
    case "DELETE_ITEM":
      sotre.deleteItem(action.id); //更新状态
      store.emitItemDeleted(); //通知视图更新
      break;
    default:
    // no op
  }
});

store 包含了应用的所有状态和逻辑,它有点像传统的 MVC 模型中的 model 层,但又与之有明显的区别,store 包括的是一个应用特定功能的全部状态和逻辑,它代表了应用的整个逻辑层;而不是像 Model 一样包含的是数据库中的一些记录和与之对应的逻辑。

参考链接:flux

Redux

原生 Redux API 最简单的用例

function counter(state, action) {
  if (typeof state === "undefined") {
    return 0;
  }

  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

var store = Redux.createStore(counter); //
var valueEl = document.getElementById("value");

function render() {
  valueEl.innerHTML = store.getState().toString();
}

render();
store.subscribe(render);

document.getElementById("increment").addEventListener("click", function() {
  store.dispatch({ type: "INCREMENT" });
});

document.getElementById("decrement").addEventListener("click", function() {
  store.dispatch({ type: "DECREMENT" });
});

document.getElementById("incrementIfOdd").addEventListener("click", function() {
  if (store.getState() % 2 !== 0) {
    store.dispatch({ type: "INCREMENT" });
  }
});

document.getElementById("incrementAsync").addEventListener("click", function() {
  setTimeout(function() {
    store.dispatch({ type: "INCREMENT" });
  }, 1000);
});

应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 改变 state 的唯一办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。

Redux 三大原则

  • 单一数据源

    整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

  • state 是只读的

    唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

  • 使用纯函数来执行修改

    为了描述 action 如何改变 state tree ,你需要编写 reducers。 改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。

严格的单向数据流是 Redux 架构的设计核心。

Redux API

Redux 的 API 非常少。

Redux 定义了一系列的约定(contract)来让你来实现(例如 reducers),同时提供少量辅助函数来把这些约定整合到一起。

Redux 只关心如何管理 state。在实际的项目中,你还需要使用 UI 绑定库如 react-redux。

  • createStore(reducer, [preloadedState], [enhancer])
  • combineReducers(reducers)
  • bindActionCreators(actionCreators, dispatch)
  • applyMiddleware(...middlewares)
  • compose(...functions)

immutable

在写 redux 的 action 的时候,总是需要用到扩展语句或者 Object.assign()的方式来得到一个新的 state,这一点对于 JavaScript 而言是对象的浅拷贝,它对内存的开销肯定是大于 mobX 中那样直接操作对象属性的方式大得多。

参考链接:redux-immutable seamless-immutable reselect 为什么使用 Redux 管理状态是可预测的

redux-saga

redux 是 react 技术栈中的状态控制流框架,使用了标准的函数式思想,期望(强制)所有状态管理都是纯函数。这也意味着各状态之间都是独立的。但是有一类状态 redux 直接甩给了的第三方模块,副作用模块 redux-saga 也就成了任劳任怨的典型代表。副作用正是因为不确定性和可变性而得名,而其给出的状态又是相互影响,如何解耦使得原本复杂的非线性呈现为线性逻辑,正是有限状态机的用武之处。

DvaJS

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

在 redux 的生态圈内,每个环节有多种方案,比如 Data 可以是 immutable 或者 plain object,在你选了 immutable 之后,用 immutable.js 还是 seamless-immutable,以及是否用 redux-immutable 来辅助数据修改,都需要选择。

参考链接:Redux 中文文档 immutable-js immer dvajs React + Redux 最佳实践

MobX

MobX 是一个用法简单优雅、同时具有可扩展性的状态管理库。

一个简单的例子

import { observable, autorun } from "mobx";

const appState = observable({
  counter: 0,
  add(value) {
    this.counter += value;
  }
});

autorun(() => console.log(appState.counter));

setInterval(() => appState.add(1), 1000);

在 mobx 中我们可以直接修改状态

import { observable } from "mobx";

const appState = observable({ counter: 0 });

appState.counter += 1;

可以通过引入 Strict 模式来避免这种不良好的实践:

import { useStrict } from "mobx";

useStrict(true);

MobX 脱胎于响应式编程(Reactive Programming),其核心思想为 Anything that can be derived from the application state, should be derived. Automatically,即避免任何的重复状态。

MobX 中核心的概念即是 Observable,相信接触过响应式编程的肯定非常熟悉,从后端的典型代表 RxJava 到 Android/iOS 开发中的各种响应式框架都各领风骚。

与 Redux 状态管理上的异同

Redux / MobX 均为客户端开源状态管理库,用状态来描述 UI 界面,它们与 React 都不具有强绑定关系,你也可以配合别的框架来使用它们。 当然,与 React 是再合适不过的了,React 作为 View 层的框架,通过 Virtual DOM 机制来优化 UI 渲染,Redux / MobX 则提供了将相应状态同步到 React 的机制。

Redux 与 MobX 的不同主要集中于以下几点:

  • Redux 是单一数据源,而 MobX 往往是多个 store。MobX 可以根据应用的 UI、数据或业务逻辑来组织 store,具体如何进行需要你自己进行权衡。
  • Redux store 使用普通的 JavaScript 对象结构,MobX 将常规 JavaScript 对象包裹,赋予 observable 的能力,通过隐式订阅,自动跟踪 observable 的变化。MobX 是观察引用的,在跟踪函数中(例如:computed value、reactions 等等),任何被引用的 observable 的属性都会被记录,一旦引用改变,MobX 将作出反应。注意,不在跟踪函数中的属性将不会被跟踪,在异步中访问的属性也不会被跟踪。
  • Redux 的 state 是只读的,只能通过将之前的 state 与触发的 action 结合,产生新的 state,因此是纯净的(pure)。而 MobX 的 state 即可读又可写,action 是非必须的,可以直接赋值改变,因此是不纯净的(Impure)。
  • Redux 需要你去规范化你的 state,Immutable 数据使 Reducer 在更新时需要将状态树的祖先数据进行复制和更新,新的对象会导致与之 connect 的所有 UI 组件都重复渲染。因此 Redux state 不建议进行深层嵌套,或者需要我们在组件中用 shouldComponentUpdate 优化。而 MobX 只自动更新你所关心的,不必担心嵌套带来的重渲染问题。
  • 在 Redux 中区分有 smart 组件与 dumb 组件,dumb 负责展示,smart 负责状态更新,数据获取。而在 MobX 中无需区分,都是 smart,当组件自身依赖的 observable 发生变化时,会作出响应。

Mobx 思想的实现原理

Mobx 最关键的函数在于 autoRun,autoRun 的专业名词叫做依赖收集,也就是通过自然的使用,来收集依赖,当变量改变时,根据收集的依赖来判断是否需要更新。Mobx 使用了 Object.defineProperty 拦截 getter 和 setter,和 Vue 一样。

参考链接: mobx MobX 中文文档

Vuex

Vuex 是专门为 Vue.js 设计的状态管理库。把组件的共享状态抽取出来,以一个全局单例模式管理。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

核心概念

  • State
  • Getter
  • Mutation
  • Action
  • Module

API

Vuex 的用法很简单,  一句话总结:commit mutation,dispatch action

参考链接:Vuex 官方文档

有限状态机(FSM)

有限状态机(finite-state machine)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型,非常有用,可以模拟世界上大部分事物。

有限状态机并不是一个复杂的概念,简单说,它有三个特征:

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另一种状态。

总结

使用状态去影响视图,而 Action 主要负责完成状态间的变更。代码如何更好的构建其核心在于使用最合理的状态去管理界面,并用最合理的动作去实现状态间的变更。

所谓的状态管理,实际上就是使用有限状态机来管理前端状态。

有限状态机对 JavaScript 的意义在于,很多对象可以写成有限状态机。

写代码之前,思考一下:

  • 页面有几种状态(初始化状态?成功状态?失败状态?出错状态?)。
  • 描述这些状态需要什么参数。
  • 在什么时候转变状态,需要改变哪些部分。

然后跟着思路,完成数据与 UI 部分。

参考链接:javascript-state-machine xstate managing-state-in-javascript-with-state-machines-stent