前端状态管理请三思
最近我开始思考React应用的状态管理。我已经取得一些有趣的结论,并且在这篇文章里我会向你展示我们所谓的状态管理并不是真的在管理状态。
原文链接:managing-state-in-javascript-with-state-machines-stent 我们避而不谈的是什么(The elephant in the room)我们来看一个简单的例子。想象这是一个展示用户名称、密码和一个按钮的表单组件。用户会在填写表单后点击提交。如果一切顺利,我们完成了登录,并且有必要展示欢迎信息和一些链接:
var isLoggedIn; isLoggedIn = false; // 展示表单 isLoggedIn = true; // 展示欢迎信息和链接 但是这样还不够。如果我们点击提交按钮后触发的HTTP请求需要一些时间来响应,我们不能把表单孤零零的放在屏幕上,而需要更多的UI元素来展示这样的中间状态,因此我们不得不在组件中引入另一个状态。
现在我们有了第三种展示状态,仅仅用一个 var isLoggedIn; var isInProgress; // 展示表单 isLoggedIn = false; isInProgress = false; // 请求过程中 isLoggedIn = false; isInProgress = true; // 展示欢迎信息和链接 isLoggedIn = true; isInProgress = false; 非常棒!我们用到两个变量并且需要记住这三种情况对应的变量值。看起来我们解决了问题。但另外的问题是,我们维护了太多状态。如果我们需要展示一个请求成功的信息,或者一切顺利的时候我们需要告知用户:“Yep,你成功登录了”,并且两秒后信息伴随着华丽的动画隐藏起来,接着展示出最终的界面,要怎么办?
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 { // 等待输入,展示表单 } 我们还有一个问题会让这个情景变得更糟:如果请求失败我们要怎么做?我们需要展示一个错误信息和一个重试链接,如果点击重试我们会重复一次请求的过程。
现在我们的代码已经没有任何可维护性。我们有非常多的场景需要满足,仅仅依赖引入新的变量是不可接受的。让我们想想是否可以通过更好的命名方式来解决,同时可能还需要引入一个新的条件声明。
我们需要第四个变量, var isRequestFinished,isSuccessful,isFailed; if (isInProgress) { // 请求过程中 } else if (isRequestFinished) { if (isSuccessful) { // 展示请求成功信息 } else if (isFailed) { // 展示请求失败信息和重试链接 } else { // 展示欢迎信息和链接 } } else { // 等待输入,展示表单 } 这四个变量描述了一个看似简单但实际并不简单的过程,这个过程包含了许多边界情况。当项目进一步迭代时,最终可能会由于已有变量的组合不能满足新的需求,而定义更多的变量。这就是构建用户界面十分困难的原因。 我们需要更好的状态管理方式。也许可以使用更现代和更流行的概念。 Flux 或者 Redux 怎么样?最近我在思考 Flux 架构和 Redux 库在状态管理中的定位。即使这些工具和状态管理有关,但是它们本质上不是解决这类问题的。
它们是 “单向数据流” 和 “状态容器”,而不是 “状态管理”。Flux 和 Redux 背后的概念是非常实用和讨巧的。我认为它们是适合构建用户界面的方式。单向数据流让数据拥有可预测性,改进了前端开发。Redux 中的 reducer 拥有的不可变特性,提供了一种可以减少 bug 的数据传送方式。 几个月之前我开始寻找可以解决状态管理问题的模式,最终我发现了状态机的概念。事实上我们一直都在构建状态机,只不过我们不知道。 什么是状态机?
状态机的数学定义是一个计算模型,我的理解是:状态机就是保存你的状态和状态变化的一个盒子。这里有一些不同种类的状态机,适用于我们这个案例的是有限状态机。像它的名字一样,有限状态机包含有限的几种状态。它接收一个输入并且基于这个输入和当前的状态决定下一个状态,可能会有多种情况输出。当状态机改变了状态,我们就称为它过渡到一个新的状态。 实战状态机为了使用状态机我们或多或少需要定义两件事 - 状态和可能的过渡方法。让我们来尝试实现上面提到的表单需求。
在这个表格中我们可以清楚的看到所有状态和他们可能的输出情况。我们同样定义了如果输入被传递进状态机后的下一个状态。编写这样的表格对你的开发周期大有裨益,因为他会回答你以下问题:
这三个问题可以解决非常多的难题。想象一下当我们改变内容展示的时候有一个动画效果,当动画开始时,UI 仍然处于之前的状态并且用户仍然可以产生交互。举个例子,用户非常快速地点击了两次提交按钮。如果不适用状态机,我们需要使用if语句通过标志变量来防止代码的执行。但是如果回到上面那个表格,我们会看到 loading 状态不接受 Submit 状态的输入。所以如果我们在第一次点击按钮后把状态机转变为 loading 状态,我们就会处于一个安全的位置。即使 Submit 输入/动作被分发过来,状态机也会忽略它,当然也不会再向后端发出一个请求。 状态机模式对我来说是适用的。以下有三个理由支撑我在我的应用中使用状态机:
在 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' } } } 这个状态机对象使用我们上面表格中的内容定义了状态。像示例中那样,当我们在 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 注意我们尝试通过在 最后的话我不知道状态机的概念是否适用于你自己的场景,但是对我来说非常适用。我仅仅改变了我处理状态管理的方式。我建议去尝试一下,绝对是值得的。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- Flex中的FusionCharts 2D折线图
- c# – 如何使用动作过滤器在asp.net mvc中集中模型验证?
- Oracle-内存管理解读-更新中
- c – boost asio服务器在调用时挂起来关闭boost :: socket
- unmanaged – Dependency Walker不显示所有依赖的Dll
- Vue中div contenteditable 的光标定位方法
- MyReport报表引擎
- Oracle Data Guard 的角色转换(Failover)
- c# – 使用asp.net中的Zero Clipboard将文本复制到剪贴板
- 在U-boot下实现自动识别启动Flash的原理(针对S3C24x0)