惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
C
Cybersecurity and Infrastructure Security Agency CISA
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
爱范儿
爱范儿
Spread Privacy
Spread Privacy
P
Palo Alto Networks Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
The Cloudflare Blog
WordPress大学
WordPress大学
小众软件
小众软件
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
H
Hacker News: Front Page
Last Week in AI
Last Week in AI
D
DataBreaches.Net
V
V2EX
S
Securelist
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Cloudbric
Cloudbric
月光博客
月光博客
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
N
News and Events Feed by Topic
K
Kaspersky official blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Secure Thoughts
S
Security Affairs
W
WeLiveSecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
AI
AI
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
罗磊的独立博客
Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
Netflix TechBlog - Medium
PCI Perspectives
PCI Perspectives
SecWiki News
SecWiki News
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog

一颗小树

周更的第 100 篇 改变阅读方式 宝贵的人生建议 小树的工具库 2023 读《重构》有感 仅需 10 分钟,用 GPTs 实现文章总结助手 高质量的需求交付 产品始于问题,而不是解决方案 如何更好地休息 最优解人生 与体重斗,其乐无穷 结构化 prompt = 数字员工? 持续创作的法门 如何降低知识焦虑 7 月思维碎片 N 倍生产力提升:我的 AI 助理 Indie Hacker,互联网打工人的下一个出路? 如何成就伟大事业 夏日碎片 小报编辑的自我修养 一颗小树 #49 投入真实生活 一颗小树 #48 过年杂记 一颗小树 #47 我的人生信念(2023) 一颗小树 #46 回本就卖 一颗小树 #45 规划和落地 小树的 2022 年终总结 一颗小树 #44 构建高质量信源 一颗小树 #43 我的 2022 书单 一颗小树 #42 阳了怎么办 一颗小树 #41 一人企业 一颗小树 #40 非必要别正常啦 一颗小树 #39 阅读和创造 一颗小树 #38 疫情生活碎片 一颗小树 #37 基于多维表格的可转债投资系统 一颗小树 #36 飞盘初体验 一颗小树 #35 雁栖湖半日游 一颗小树 #34 初探多维表格 一颗小树 #33 回不去的名字叫家乡 一颗小树 #32 生活需要留白 一颗小树 #31 Figma 的成功之道 一颗小树 #30 拓宽自己 拥抱善意 一颗小树 #29 有关下雨的记忆 一颗小树 #28 隐入尘烟 一颗小树 #27 找到并坚持自己的热爱 一颗小树 #26 好产品的“差”改版 一颗小树 #25 吹吹南澳岛的海风 一颗小树 #24 代码重构的一点心得 一颗小树 #23 和团队新同学的对话 一颗小树 #22 从《干法》中学到的 一颗小树 #21 阿里四年我的收获和感想 一颗小树 #20 周更半年我学到的事 一颗小树 #19 慢慢来比较快 一颗小树 #18 只规划每天最重要的三件事 一颗小树 #17 裁员潮下的反思 一颗小树 #16 纪录片的力量在于真实 一颗小树 #15 世界很小但很好 一颗小树 #14 让自己成为问题的终结者 一颗小树 #13 别忘记你的热爱 一颗小树 #12 聊聊写作这件事 一颗小树 #11 开始投资前更重要的事 一颗小树 #10 疫情囤货指南 一颗小树 #9 疫情之下 重建内心的秩序 一颗小树 #8 不要倒在黎明前 一颗小树 #7 从小孩子身上学到的事 一颗小树 #6 和自己的产品一起成长 一颗小树 #5 技术对生产力的的变革是不可阻挡的 一颗小树 #4 暴风雨的意义 一颗小树 #3 投资成功,是我们变成一个更好的人之后自然的结果 一颗小树 #2 持续学习来应对这个复杂多变的世界 一颗小树 #1 任天堂和上岭村 一颗小树 #0 这是写给你们的第一封信 游戏改变世界 把自己给开源了 构建自己的不为清单 有爱和被爱 做了一点微小的工作 小树的 2021 年终总结 小树的 2020 年终总结 与其感慨路难行,不如马上出发 阿里前端也切图?不,人工智能帮你做 如何写好简历,拿到大厂 offer 的敲门砖 任时光匆匆流去 | 2019 年终总结 聊聊在阿里远程办公那点事儿 在 D2 做主持人是怎样一种体验? 提高工作效率的几个方法 有限状态机在 CSS 动画中的应用 「译」React 项目结构和组件命名之道 编写小而美函数的艺术 使用纯粹的JS构建 Web Component 利用Dawn工程化工具实践MobX数据流管理方案 高性能 MobX 模式(part 3)- 用例教程 高性能 MobX 模式(part 2)- 响应变化 技术文章写法浅谈 翻译计划-用node.js开发一个可交互的命令行应用 无他,唯手熟尔-前端实习面试题篇 工欲善其事必先利其器-前端实习简历篇 手把手带你用原生js实现css属性的set和get 我们来翻翻元素样式的族谱-getComputedStyle 翻译计划-从sass到PostCSS 把简单做好也不简单-css水平垂直居中
翻译计划-状态管理请三思
也树 · 2017-11-04 · via 一颗小树

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

译者:阿里云前端-也树

原文链接:managing-state-in-javascript-with-state-machines-stent

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

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

login_1

我们假定这个组件有两个展示状态。一个是未登录状态,另一个是用户登录后的状态。所以从管理这两种状态开始,我们用一个布尔值的标志位来描述用户的状态。

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

但是这样还不够。如果我们点击提交按钮后触发的 HTTP 请求需要一些时间来响应,我们不能把表单孤零零的放在屏幕上,而需要更多的 UI 元素来展示这样的中间状态,因此我们不得不在组件中引入另一个状态。

login_2

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

var isLoggedIn;
var isInProgress;

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

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

// 展示欢迎信息和链接
isLoggedIn = true;
isInProgress = false;

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

login_3

现在情况变得有些复杂。我们有了 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 {
  // 等待输入,展示表单
}

我们还有一个问题会让这个情景变得更糟:如果请求失败我们要怎么做?我们需要展示一个错误信息和一个重试链接,如果点击重试我们会重复一次请求的过程。

login_4

现在我们的代码已经没有任何可维护性。我们有非常多的场景需要满足,仅仅依赖引入新的变量是不可接受的。让我们想想是否可以通过更好的命名方式来解决,同时可能还需要引入一个新的条件声明。

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 请求我们可以通过某个库来解决,但是对其它相关的业务逻辑我们仍然需要自己编写代码来实现。问题在于我们如何用一种合适的方式去组织这些代码,而不至于每两年就把整个应用重写一遍。

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

什么是状态机?

fsm

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

实战状态机

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

Finite State Machine table

在这个表格中我们可以清楚的看到所有状态和他们可能的输出情况。我们同样定义了如果输入被传递进状态机后的下一个状态。编写这样的表格对你的开发周期大有裨益,因为他会回答你以下问题:

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

这三个问题可以解决非常多的难题。想象一下当我们改变内容展示的时候有一个动画效果,当动画开始时,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 状态来打破状态机的运转或者是重复发送提交请求。在这些场景下,当前的状态没有被改变并且状态机会忽略这些输入。

最后的话

我不知道状态机的概念是否适用于你自己的场景,但是对我来说非常适用。我仅仅改变了我处理状态管理的方式。我建议去尝试一下,绝对是值得的。