状态管理请三思
译者:阿里云前端-也树
咱们避而不谈的是什么(The elephant in the room)
咱们来看一个简略的例子。幻想这是一个展现用户名称、暗码和一个按钮的表单组件。用户会在填写表单后点击提交。假如一切顺利,咱们完成了登录,而且有必要展现欢迎信息和一些链接:
咱们假定这个组件有两个展现状况。一个是未登录状况,另一个是用户登录后的状况。所以从办理这两种状况开端,咱们用一个布尔值的标志位来描述用户的状况。
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, 你成功登录了”,而且两秒后信息伴随着华丽的动画隐藏起来,接着展现出最终的界面,要怎么办?
现在状况变得有些杂乱。咱们有了 isLoggedIn 和 isInProgress,可是看起来只是运用它们还不行。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 状况来打破状况机的工作或许是重复发送提交恳求。在这些场景下,当时的状况没有被改动而且状况机会忽略这些输入。
最后的话
我不知道状况机的概念是否适用于你自己的场景,可是对我来说十分适用。我只是改动了我处理状况办理的方法。我主张去测验一下,肯定是值得的。
Redux 是一个可猜测的状况容器,用来构建 JavaScript 运用。
就我的感触来说,这些形式更适用于数据办理和数据流办理。它们供给了完善的 API 来交流改动咱们运用数据的信息,可是并不能处理咱们状况办理的问题。这也因为这些问题是跟项目强相关的,问题的上下文取决于咱们正在做的事情。
当然像处理 HTTP 恳求咱们能够经过某个库来处理,可是对其它相关的事务逻辑咱们依然需求自己编写代码来完成。问题在于咱们怎么用一种适宜的方法去安排这些代码,而不至于每两年就把整个运用重写一遍。
我有话说: