当前位置:   article > 正文

前端:从状态管理到有限状态机的思考

前端如何使用状态机维护状态流转

点击上方 程序员成长指北,回复“1”

加入Node进阶交流群

1. 状态管理

在我们前端开发中,一定会接触现在最热门的几大框架(Vue, React等等),在使用框架的过程中,我们一定会接触某些状态管理工具。

Vue我们会使用Vuex来管理全局状态, React会使用Redux来管理。

首先是不是,在问为什么?

在使用类似Vue,React框架时,我们一定会使用状态管理吗?这个答案是肯定的。或许我不会主动去使用Vuex, Redux,但我们编写每一个组件的时候就已经在管理状态,Vuex, Redux只是更方便我们进行全局的状态管理。

为什么一定会使用状态管理?这是因为现代前端框架使用数据驱动视图的形式来描述页面。比如,Vue、 React组件会有一个自己内部,外部的状态来共同决定组件的如何显示的,用户与组件交互导致数据变更,进而改变视图。

框架内部状态外部状态
Vuedataprops
Reactstate, useStateprops

所以我们所写大部分业务逻辑,是在管理状态,框架会帮我们状态映射成视图,这可以说是很经典的MVVM模式。

  1. View = ViewModel(Model);
  2. // 视图 =  状态 + 管理
  3. 复制代码

2. 有限状态机:计算机中一种用来进行对象行为建模的工具

其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。

我们来理解一下上面这段话。

  • 一种对象行为建模工具

    我们用来描述对象行为,状态随着时间转变过渡行为的工具。可以模拟世界上大部分事物。

  • 生命周期

    我们通过抽象对象所经历的状态序列,来确定对象一系列可能的生命周期和转变。

  • 响应外界事件

    外界事件能够影响对象内部状态。对象能够对外部事件作出响应。

状态机有基本几个要素:

  1. 当前所处状态

    在各个时刻只处于一种状态

  2. 状态转移函数

    在某种条件下,会从一种状态转移到另外一种状态。

  3. 有限状态序列

    拥有有限,可枚举的状态数

bg2013090201.png

上面这张图所描述的状态机,我们使用js对象来进行描述

  1. const stateTool = {
  2.   // 当前状态
  3.   currentState: '1',
  4.   
  5.   // 状态转变函数
  6.   transition: (event) => {
  7.   switch(event.type) {
  8.       case '1': {
  9.         this.currentState = event.status;
  10.         doSomething1();
  11.         break;
  12.       }
  13.       case '2': {
  14.         this.currentState = event.status;
  15.         doSomething2();
  16.         break;
  17.       }
  18.       case '3': {
  19.         this.currentState = event.status;
  20.         doSomething3();
  21.         break;
  22.       }
  23.       default
  24.         console.log('Invalid State!');
  25.         break;
  26.     }
  27.   }
  28. }
  29. 复制代码

使用有限自动机是一种状态管理的思考方式,我们可以列举组件状态列表,设计触发状态函数。通过外部或内部交互行为,触发函数改变状态,根据状态改变视图

3. Flux思想

Flux是什么?Flux是一个Facebook开发的、利用单向数据流实现的应用架构

简单说,Flux 是一种架构思想,专门解决软件的结构问题。可以说他是有限状态机的另外一种形式。

一个Flux管理分为4个状态:

  • View:视图层

  • Action(动作):视图层触发的动作 or 行为

  • Dispatcher(派发器):收集触发行为,进行统一管理,统一分发给store层。

  • Store(数据层):用来存放应用的状态,根据dispatcher触发行为,就提醒Views要更新页面

bg2016011503.png

要是同学了解flux的的工作流程,那么很容易就发现这是一种工程化的状态机。

  • 初始状态

我们通过store 存放的是初始化状态,这种初始化状态数据可以页面初始化时设定 或 页面加载时请求后端接口数据,来初始化store数据。

通过store的初始化数据,来构建初始化的视图层。

  • 状态转移事件

根据视图层的行为会触发action,我们通过统一的dispatcher来收集action, dispatcher将行为派发给store。

  • 状态转移函数

store通过判断事件的类型 和 payload,来修改内部存储状态。达到状态转移的目的,并统一提醒view层更新页面;

4. 全局到局部的状态管理

既然我们是通过数据状态来管理视图的,那么在设计初期我们就可以从有限的状态转移来思考业务逻辑。通过思考每个状态对应的数据,状态转移函数,我们可以很清晰的罗列出数据更变逻辑。从数据去控制视图也是现代前端所接触到的MVVM模式。

一个大型应用,我们也会使用Vuex 或 Redux来进行一整个应用的管理。

在平时的业务中,我们会遇到一个痛点是:Vuex,Redux是一个全局状态管理,但我们现在需要在局部需要一个局部状态管理变更,只能使用mutation 或 dispatch去提交更改。

如果我们频繁的更新状态,那么我们需要为每一个局部模块编写大量dispatch函数来间接修改全局状态。随着应用的扩充,dispatch文件会越来越臃肿。

那么我们是不是可以使用不同的状态管理工具,来实现局部状态的管理。在局部状态更新完之后,再去用局部更新去更新全局呢?

注:但这也会有一个缺点,局部管理相对独立。有些高度复用的提交函数需要放在全局状态管理上

a. 框架原生组件状态管理

React Hooks + React.createContext

React Hooks提供了useReducer + useContext + Context 可以实现一个小型的状态管理

  1. // 以下代码就实现了一个能够穿透组件的状态管理
  2. import React, { useReducer, useContext } from 'react';
  3. const reducer = (state = 0, { type, ...payload }) => {
  4.   switch (type) {
  5.     case 'add':
  6.       return state + 1;
  7.     case 'desc':
  8.       return state - 1;
  9.     default:
  10.       return state;
  11.   }
  12. }
  13. const Context = React.createContext();
  14. const Parent = () => {
  15.   const [state, dispatch] = useReducer(reducer, 0);
  16.   return (
  17.     <>
  18.       <Context.Provider value={{ state, dispatch }}>
  19.         <Son />
  20.       </Context.Provider>
  21.     </>
  22.   )
  23. }
  24. function Son() {
  25.   return <Counter />
  26. }
  27. function Counter() {
  28.   const { state, dispatch } = useContext(Context);
  29.   return (
  30.     <div>
  31.       <button onClick={() => dispatch({ type'desc' })}>-</button>
  32.       {state}
  33.       <button onClick={() => dispatch({ type'add' })}>+</button>
  34.     </div>
  35.   )
  36. }
  37. export default Parent;
  38. 复制代码

Vue响应式数据 + vue.Provide/inject

使用vue响应式系统 + provide/inject API来实现一个具有穿透性的局部状态管理

  1. // Parent.vue
  2. <template>
  3. <Son />
  4. </template>
  5. <script setup>
  6. import { provide, reactive, readonly } from "vue";
  7. import Son from "./Son.vue";
  8. const data = reactive({
  9. count: 0,
  10. });
  11. const onAdd = () => {
  12. data.count++;
  13. };
  14. const onDesc = () => {
  15. data.count--;
  16. };
  17. provide("store", {
  18. data: readonly(data), // 只读属性
  19. onAdd, // 修改函数add
  20. onDesc, // 修改函数desc
  21. });
  22. </script>
  23. 复制代码
  1. // Son.vue
  2. <template>
  3. <Counter />
  4. </template>
  5. <script setup>
  6. import Counter from "./Counter.vue";
  7. </script>
  8. 复制代码
  1. // Counter.vue
  2. <template>
  3. <div>
  4. <button @click="store.onDesc">-</button>
  5. {{ store.data.count }}
  6. <button @click="store.onAdd">+</button>
  7. </div>
  8. </template>
  9. <script setup>
  10. import { inject } from "vue";
  11. const store = inject("store", {}); // 穿透读取store
  12. </script>
  13. 复制代码

b. 线性状态管理:Xstate

0*3QzqRMfRCh28-xe1..png

Xstate是一个很有趣的类似有限状态机的状态管理,Xstate着重点在于管理状态,通过状态转换去维护数据

我们来定义一个简单的promise状态机,使用官方提供的工具进行可视化

image-20210417125405879.png
  1. import { Machine } from 'xstate';
  2. // 创建状态机
  3. const promiseMachine = Machine({
  4.   id: 'promise'// 唯一id
  5.   initial: 'pending'// 初始化状态
  6.   states: { // 状态集合
  7.     pending: {
  8.       on: {
  9.         RESOLVE: 'resolved',
  10.         REJECT: 'rejected',
  11.    }
  12.     },
  13.     resolved: {
  14.       type'final',
  15.     },
  16.     rejected: {
  17.       type'final'
  18.     }
  19.   }
  20. })
  21. 复制代码

注意⚠️:状态机不拥有状态,他只是定义状态和定义状态转移

Xstate有提供函数来实现状态机服务,实现拥有状态的实体

  1. import { interpret } from 'xstate'
  2. const promiseService = interpret(promiseMachine).onTransition(state => 
  3.   console.log(state.value)
  4. // 创建服务,指定状态转移时回调函数
  5. promiseService.start() // 启动服务
  6. promiseService.send('RESOLVE'); // 通知服务转移状态,并执行回调函数
  7. 复制代码

这样子我们就实现了一个简单的Promise状态机。他有很多应用,可以结合Vue,结合React进行使用。更加深入的内容就需要到官方文档中自行探索了!

就我个人的看法,状态机思想非常适合状态转移相对线形的场景,在某些状态多循环的场景转移会相对复杂些

c. 可响应式的状态管理器:Mobx

mobx是一种响应式的状态管理,他所提倡的是拆分store做数据管理。这就很适合做局部的状态管理,根据局部状态管理来更新全局状态。

相同的,我们举个例子

  1. import { action, autorun, observable } from 'mobx'
  2. import { observer } from 'mobx-react'
  3. import React from 'react'
  4. const appStore = observable({ // 建立store
  5.   count: 0,
  6.   age: 18,
  7. })
  8. // autorun 只会观察依赖的相关数据
  9. // 使用当appStore.age更新时,才会触发该函数
  10. autorun(() => {
  11.   // doSomething();
  12.   console.log('autorun', appStore.age);
  13. })
  14. const Counter = observer(() => {
  15.   const { count } = appStore;
  16.   const onAdd = action(() => { // 使用action更新store数据
  17.     appStore.count++;
  18.   })
  19.   const onDesc = action(() => {
  20.     appStore.count--;
  21.   })
  22.   return (
  23.     <div>
  24.       <button onClick={onDesc}>-</button>
  25.       {count}
  26.       <button onClick={onAdd}>+</button>
  27.     </div>
  28.   )
  29. })
  30. export default Counter;
  31. 复制代码

5. 总结

现在前端主流使用数据驱动视图的形式,来实现业务。希望给大家带来两点启发

  1. 用有限状态机去思考某些线性状态场景的数据管理。

  2. 在之前的业务开发的时候,就会出现一个痛点,应用全局状态管理非常臃肿。

    在不断功能迭代的过程中,需要做不同的状态管理,虽然都是对同一份数据进行维护,但维护的方式不同,进行一次状态更新就需要编写一个不同的dispatch函数。随着业务需求的增加,dispatch函数越来越多,难以管理和复用。

    思考如何解决这个问题的时,偶然看到了有限状态机相关文章,思考到应用的功能模块在某一个时刻是相互独立的,我们在局部将数据进行更新,之后用一个全局函数对数据进行统一替换。

注:本文为探索性质,使用原生组件进行局部管理不需要引入依赖。但使用第三方工具造成包体积大小的增加,是否会增加性能消耗有待讨论

参考资料

  • 可视化工具: xstate.js.org/viz/

  • mobx中文文档: cn.mobx.js.org/

  • Xstate文档: xstate.js.org/

  • 浅谈对比Xstate、redux使用: juejin.cn/post/684490…

  • 前端状态管理与有限状态机: juejin.cn/post/684490…

  • 状态管理新思路: 有限状态机载前端的应用: mp.weixin.qq.com/s/Zt1kXj5ts…

关于本文

气旋

https://juejin.cn/post/6952047570046697485

最后

如果觉得这篇文章还不错

点击下面卡片关注我

来个【分享、点赞、在看】三连支持一下吧

   “分享、点赞、在看” 支持一波  

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/696541
推荐阅读
相关标签
  

闽ICP备14008679号