前端状态管理请三思

最近我开始思考React应用的状态管理。我已经取得一些有趣的结论,而且在这篇文章里我会向你展现咱们所谓的状态管理并非真的在管理状态。javascript

译者:阿里云前端-也树前端

原文连接:managing-state-in-javascript-with-state-machines-stentjava

咱们避而不谈的是什么(The elephant in the room)

咱们来看一个简单的例子。想象这是一个展现用户名称、密码和一个按钮的表单组件。用户会在填写表单后点击提交。若是一切顺利,咱们完成了登陆,而且有必要展现欢迎信息和一些连接:web


咱们假定这个组件有两个展现状态。一个是未登陆状态,另外一个是用户登陆后的状态。因此从管理这两种状态开始,咱们用一个布尔值的标志位来描述用户的状态。后端

var isLoggedIn;
isLoggedIn = false; // 展现表单
isLoggedIn = true; // 展现欢迎信息和连接

可是这样还不够。若是咱们点击提交按钮后触发的HTTP请求须要一些时间来响应,咱们不能把表单孤零零的放在屏幕上,而须要更多的UI元素来展现这样的中间状态,所以咱们不得不在组件中引入另外一个状态。安全

如今咱们有了第三种展现状态,仅仅用一个 isLoggedIn 变量已经不能解决了。不走运的是咱们不能设置变量值为 false-ish,它不是 true 也不是 false。固然,咱们能够引入另外一个变量好比说 isInProgress。一旦咱们发送请求就会把这个变量的值置为 true。这个变量会告诉咱们是处于请求的过程当中而且用户应该看到加载中的展现状态。服务器

var isLoggedIn;
var isInProgress; 

// 展现表单
isLoggedIn = false;
isInProgress = false;

// 请求过程当中
isLoggedIn = false;
isInProgress = true;

// 展现欢迎信息和连接
isLoggedIn = true;
isInProgress = false;

很是棒!咱们用到两个变量而且须要记住这三种状况对应的变量值。看起来咱们解决了问题。但另外的问题是,咱们维护了太多状态。若是咱们须要展现一个请求成功的信息,或者一切顺利的时候咱们须要告知用户:“Yep, 你成功登陆了”,而且两秒后信息伴随着华丽的动画隐藏起来,接着展现出最终的界面,要怎么办?架构


如今状况变得有些复杂。咱们有了 isLoggedInisInProgress,可是看起来仅仅使用它们还不够。isInProgress 在请求结束后确实是 false,可是他的默认值一样是 false。我以为咱们须要第三个变量 - isSuccessful函数

var isLoggedIn, isInProgress, isSuccessful;

// 展现表单
isLoggedIn = false;
isInProgress = false;
isSuccessful = false;

// 请求过程当中
isLoggedIn = false;
isInProgress = true;
isSuccessful = false;

// 展现成功状态
isLoggedIn = true;
isInProgress = false;
isSuccessful = true;

// 展现欢迎信息和连接
isLoggedIn = true;
isInProgress = false;
isSuccessful = false;

咱们简单的状态管理一步步变成了由 if-else 组成的巨大的条件网,很难去理解和维护。工具

if (isInProgress) {
  // 请求过程当中
} else if (isLoggedIn) {
  if (isSuccessful) {
    // 展现请求成功信息
  } else {
    // 展现欢迎信息和连接
  }
} else {
  // 等待输入,展现表单
}

咱们还有一个问题会让这个情景变得更糟:若是请求失败咱们要怎么作?咱们须要展现一个错误信息和一个重试连接,若是点击重试咱们会重复一次请求的过程。

如今咱们的代码已经没有任何可维护性。咱们有很是多的场景须要知足,仅仅依赖引入新的变量是不可接受的。让咱们想一想是否能够经过更好的命名方式来解决,同时可能还须要引入一个新的条件声明。

isInProgress 仅仅在请求的过程当中被用到。咱们如今还关心请求结束以后的过程。

isLoggedIn 有一点误导的含义,由于咱们只要请求结束就把它置为 true。而若是请求出错,用户并无真正登入。因此咱们把它重命名为 isRequestFinished。虽然看起来好些了,可是它仅仅表明咱们从服务器得到了响应,并不能用它来判断响应是否为错误。

isSuccessful 是一个最终状态合适的候选变量。若是请求出错咱们能够把它设置为 false,可是等等,它的默认值也是 false。因此它也不能做为表明错误状态的变量。

咱们须要第四个变量,isFailed 怎么样?

var isRequestFinished, isInProgress, isSuccessful, isFailed;

if (isInProgress) {
  // 请求过程当中
} else if (isRequestFinished) {
  if (isSuccessful) {
    // 展现请求成功信息
  } else if (isFailed) {
    // 展现请求失败信息和重试连接
  } else {
    // 展现欢迎信息和连接
  }
} else {
  // 等待输入,展现表单
}

这四个变量描述了一个看似简单但实际并不简单的过程,这个过程包含了许多边界状况。当项目进一步迭代时,最终可能会因为已有变量的组合不能知足新的需求,而定义更多的变量。这就是构建用户界面十分困难的缘由。

咱们须要更好的状态管理方式。也许可使用更现代和更流行的概念。

Flux 或者 Redux 怎么样?

最近我在思考 Flux 架构和 Redux 库在状态管理中的定位。即便这些工具和状态管理有关,可是它们本质上不是解决这类问题的。

Flux 是 Facebook 用来构建客户端 web 应用的架构。它利用单向数据流补足了 React 的视图组件的组织方式。

Redux 是一个可预测的状态容器,用来构建 JavaScript 应用。

它们是 “单向数据流” 和 “状态容器”,而不是 “状态管理”。Flux 和 Redux 背后的概念是很是实用和讨巧的。我认为它们是适合构建用户界面的方式。单向数据流让数据拥有可预测性,改进了前端开发。Redux 中的 reducer 拥有的不可变特性,提供了一种能够减小 bug 的数据传送方式。
就个人感觉来讲,这些模式更适用于数据管理和数据流管理。它们提供了完善的 API 来交换改变咱们应用数据的信息,可是并不能解决咱们状态管理的问题。这也由于这些问题是跟项目强相关的,问题的上下文取决于咱们正在作的事情。
固然像处理 HTTP 请求咱们能够经过某个库来解决,可是对其它相关的业务逻辑咱们仍然须要本身编写代码来实现。问题在于咱们如何用一种合适的方式去组织这些代码,而不至于每两年就把整个应用重写一遍。

几个月以前我开始寻找能够解决状态管理问题的模式,最终我发现了状态机的概念。事实上咱们一直都在构建状态机,只不过咱们不知道。

什么是状态机?

状态机的数学定义是一个计算模型,个人理解是:状态机就是保存你的状态和状态变化的一个盒子。这里有一些不一样种类的状态机,适用于咱们这个案例的是有限状态机。像它的名字同样,有限状态机包含有限的几种状态。它接收一个输入而且基于这个输入和当前的状态决定下一个状态,可能会有多种状况输出。当状态机改变了状态,咱们就称为它过渡到一个新的状态。

实战状态机

为了使用状态机咱们或多或少须要定义两件事 - 状态和可能的过渡方法。让咱们来尝试实现上面提到的表单需求。

在这个表格中咱们能够清楚的看到全部状态和他们可能的输出状况。咱们一样定义了若是输入被传递进状态机后的下一个状态。编写这样的表格对你的开发周期大有裨益,由于他会回答你如下问题:

  • 用户界面可能出现的全部状态有哪些?
  • 每种状态之间会发生什么?
  • 若是某种状态改变,结果是什么?

这三个问题能够解决很是多的难题。想象一下当咱们改变内容展现的时候有一个动画效果,当动画开始时,UI 仍然处于以前的状态而且用户仍然能够产生交互。举个例子,用户很是快速地点击了两次提交按钮。若是不适用状态机,咱们须要使用if语句经过标志变量来防止代码的执行。可是若是回到上面那个表格,咱们会看到 loading 状态不接受 Submit 状态的输入。因此若是咱们在第一次点击按钮后把状态机转变为 loading 状态,咱们就会处于一个安全的位置。即便 Submit 输入/动做被分发过来,状态机也会忽略它,固然也不会再向后端发出一个请求。

状态机模式对我来讲是适用的。如下有三个理由支撑我在个人应用中使用状态机:

  • 状态机模式免去了不少可能出现的 bug 和奇怪的清洁,由于它不会让 UI 变化为咱们不知道的状态。
  • 状态机不接受没有明肯定义的输入做为当前的状态。这会免去咱们对其它代码执行的部分容错处理。
  • 状态机强制开发者以声明式的方式思考。由于咱们大部分的逻辑须要提早定义。

在 JavaScript 里实现状态机

如今,既然咱们知道什么是状态机,那就让咱们来实现一个而且解决咱们一开始的问题。用一些嵌套的属性定义一个简单的对象字面量。

const machine = {
  currentState: 'login form',
  states: {
    'login form': {
      submit: 'loading'
    },
    'loading': {
      success: 'profile',
      failure: 'error'
    },
    'profile': {
      viewProfile: 'profile',
      logout: 'login form'
    },
    'error': {
      tryAgain: 'loading'
    }
  }
}

这个状态机对象使用咱们上面表格中的内容定义了状态。像示例中那样,当咱们在 login form 状态时,咱们用 submit 做为一个输入而且应该以 loading 状态结束。如今咱们须要一个接收输入的函数。

const input = function (name) {
  const state = machine.currentState;

  if (machine.states[state][name]) {
    machine.currentState = machine.states[state][name];
  }
  console.log(`${ state } + ${ name } --> ${ machine.currentState }`);
}

咱们得到了当前状态而且检查提供的input是否合法,若是经过检查,咱们就改变当前的状态,或者换句话说,将状态机过渡到一个新的状态。咱们提供了一个日志输出用来输入、当前状态和新的状态(若是有变化的话)。下面是如何去使用咱们的状态机:

input('tryAgain');
// login form + tryAgain --> login form

input('submit');
// login form + submit --> loading

input('submit');
// loading + submit --> loading

input('failure');
// loading + failure --> error

input('submit');
// error + submit --> error

input('tryAgain');
// error + tryAgain --> loading

input('success');
// loading + success --> profile

input('viewProfile');
// profile + viewProfile --> profile

input('logout');
// profile + logout --> login form

注意咱们尝试经过在 login form 状态的时候发送 tryAgain 状态来打破状态机的运转或者是重复发送提交请求。在这些场景下,当前的状态没有被改变而且状态机会忽略这些输入。

最后的话

我不知道状态机的概念是否适用于你本身的场景,可是对我来讲很是适用。我仅仅改变了我处理状态管理的方式。我建议去尝试一下,绝对是值得的。

相关文章
相关标签/搜索