阅读 452

从React Redux的实际业务场景来看有限状态机

写在前面

上一篇:从Promise的实现来看有限状态机

上一篇讲到了一个简单的,利用了有限状态机的前端实现PromisePromise的有限状态机除了start以及finish两个状态,其核心的三个状态其实就是一个异步行为的三种状态:PENDINGFULFILLEDREJECTED。通过异步行为的状态转移,Promise提供了一种将嵌套回调函数的调用方式尽量地扁平化,形成了一个链式的异步操作到同步操作的状态转换

除了Promise,前端还有很多的方案是基于有限状态机数学模型来进行实现的。这次来结合实际业务,稍微聊聊目前前端数据状态管理的最热门的方案Redux吧。

React & Redux

ReduxFlux

说到Redux,就不得不说起Flux。Flux说到底还是一种数据管理的理论,并且有很多种的实现。一般见到的都是facebook推广的实现方案。Flux理论的核心就是单向数据流,也就是所有的前端数据都是单向流动的。

前端的数据改变,一般都是由一些用户操作触发的。当某个用户操作,触发了某种状态的改变。像蝴蝶效应一样,又引起了其他组件或者模块的状态改变;或者是触发了某些数据请求,在异步的请求完成之后,新的数据被返回到前端,前端根据这些新的数据,进行页面状态的改变。

Flux将这整个过程拆分为几个部分:

  1. Action:状态改变的source,触发Dispatcher;
  2. Dispatcher:接收Action,负责实际的状态改变行为;
  3. Store:数据或者状态的存储器,并且在状态改变的时候,将内容分发到每个组件;
  4. View:实际触发Action的模块,是和用户进行交互的部分。

这四个因素形成一个数据或者说是状态流动的闭环,并且状态在这个闭环中流动都是单向的。

Redux和Flux的数据流过程基本一致,两者最大的不同在于Redux依赖Reducer来进行实际上的状态修改,通过一个pure function来返回一个新的状态,并且和旧的状态进行合并,来触发View的重新渲染。

这个状态转移过程是不是和上篇文章中说的有限状态机的思想有点类似呢?是的,这里的数据就是状态机中的状态

ReactRedux

作为目前最为热门的MVC前端框架,React本身具有多少优雅的特性就不再赘言了。繁琐的状态管理可能让很多人在进行React开发的时候,需要很长时间来进行组件的拆分以及重构。

在业务环境中,没有人能够在一开始就设计一个完美的组件树结构,尤其是对于较大型的业务页面。随着需求的修改(。。。),随着后端的接口的变化(。。。。。。。),你开始设计出来的组件结构以及状态总是出现漏洞,然后修修补补提测了,上线了。当你进行二次开发,或者新的迭代的时候,你就会发现原本的组件结构让你想死的心都有了,然后不停地重构,再重构。

React所有组件的变化都依赖state以及props两个对象,一个来自于外部,一个是自驱动发生改变的。

其实React的stateprops也是有限状态机中的状态,而每个具有状态的React组件,就是一个独立的状态机。

Redux为React提供了一种将分布在各个组件中的state进行统一管理的思想或者说是工具。

但是Redux存在很多问题,其中最为显著的就是如果将整个页面所有的可变状态收集到一个store中保存,这个store可能会变得很庞大,某些无用的状态导致store中存在很多冗余。

通过有限状态机组织React Redux的复杂业务逻辑

先描述一个业务场景:我们需要一个很长的活动页面,这个页面中有展示、评论、点赞、抽奖、试听等多个模块,首屏数据通过batch从服务端获取,之后的每次数据请求都是通过单独的接口进行的。

业务分解

上面的这些模块,有哪些需要通过状态机的模型来进行状态管理呢?

一般来说,无状态的展示组件是不需要进行状态管理的,所以展示、试听这种模块是不需要的。

其次,点赞是一个布尔的变化,只有成功和失败两种状态,也不需要我们如此费心去管理状态。

那么评论可能争议很大,看似比较复杂的模块其实并没有太多的逻辑,可能评论区域显示的内容很多,但大多都是静态内容,也没有太多的状态改变,并且评论部分数据量比较大,如果采用Redux会导致store的结构不容易扁平化,造成组件性能损失。

剩下的就是抽奖了,抽奖的状态非常多,比较难以管理,并且数据内容较少,很适合进行集中式地状态管理,随着业务的迭代,抽奖可能会发展出多种抽奖条件,这样修改源代码的难度也会较大。整个业务逻辑可能就变成了下面这样。

绘制抽奖状态机

为了让大家能够理解抽奖的大致流程,这里就不文字进行描述过程了,我们可以直接将抽奖部分的所有状态抽出来,每个状态作为一个单独的状态机状态,然后绘制出抽奖这个模块的状态转移过程:

Start状态可以忽略,仅仅是一个描述的起点,在前端可以看做是拿到数据之前的页面状态。

抽奖模块的核心就是一个简单的按钮,通过上面的状态机可以看到,这个按钮的文案状态总共有5种,每一种对应的操作都是不一样的。

通过这五种状态,我们可以将一个按钮的功能拆分成3个部分:文案(text)、样式(style)以及点击事件(clickCallback)

五种状态可以直接映射到3个实际的部分:

立即抽奖

  1. Text:立即抽奖
  2. Style:active
  3. clickCallback:完成抽奖操作,向后端进行数据请求,并且根据之后的结果进行状态转移,抽中转移到状态填写收货地址中。未抽中则可以根据剩余次数转移状态到立即抽奖或者开通XX抽奖中。

开通XX抽奖

  1. Text:开通XX立即抽奖
  2. Style:active
  3. clickCallback:跳转到开通XX的页面,引导用户开通XX来进行抽奖,开通则状态转移至立即抽奖,否则保留当前状态

抽奖活动过期

  1. Text:抽奖活动过期
  2. Style:disable
  3. clickCallback:() => {}

填写收货地址

  1. Text:填写收货地址
  2. Style:active
  3. clickCallback:弹出填写收货地址的对话框,并且在用户填写完成之后,状态转移到确认收货地址中,如果用户长时间未填写,则状态转移到抽奖活动过期中。

确认收货地址

  1. Text:确认收货地址
  2. Style:active
  3. clickCallback:弹出确认收货地址的对话框,并且在用户二次确认完成之后,状态转移到Finish中,如果用户长时间未确认,则状态转移到抽奖活动过期中。

愉快地Coding

如果没有这个状态机,你的代码会写成什么样子呢?

无数的if,或者是看起来很工整,但是全是冗余的switch case

现在你可以愉快地Coding了,如果你用的是React,你会发现这整个逻辑可以完成抽出成为一个单独的HOC(高阶组件)。无论以后产品狗们给你加多少的业务逻辑或者状态,你的抽奖模块就永远只需要修改一个map对象,或者一个switch case,这个HOC似乎就是完成与其他内容隔离的东西。

假设后端针对每一种状态给我们的数据是一个统一的对象:

{
    userId: 123,
    count: 20,  // 剩余抽奖次数
    expireTime: 20901831313,   // 抽奖过期时间
    lottery: {
        awardName: '拖鞋',  // 奖品名称
    },
    isWinning: false,  // 上次是否抽中
    address: {
        name: null,
        address: null
    }
}
复制代码

根据这个对象我们就可以得到当前状态以及状态转移了。

我们的HOC接收一个对象以及一个组件作为参数,当然对象就是上面后端给到的数据对象,而组件就是无状态的抽奖按钮组件了。

// action.js
// 在action中完成所有的状态转移
const lottery = (state) => {
    return dispatch => {
        return fetch('/api/lottery').then(res => {
            // 根据抽奖状态进行状态转移
            if (res.code === 200) {
                dispatch(hasQulification(res));
            } else {
                dispatch(lotteryExpire());
            }
        })
    }
}

// reducer.js
const lottery = (state = {
  	count: 0,
    status: 'noQualification'
}) {
    switch (action.type) {
        case HAS_QUALIFICATION:
            return {
                status: 'hasQualification',
                count: state.count
            };
        // ....其他状态
    }
}

// lotteryHOC.js

export default (Component, lotteryData) => {
    const statusMap = {
        'hasQulification': {
            text: '立即抽奖',
            clazz: 'active',
            cb: () => {
                dispatch(lottery()); // 请求下次抽奖结果
            }
        },
        'noQulification': {
            // 下面的代码就省略了,这个对象就是用来Map状态到实际的样式和行为的
        }
    }
    return class Wrapper extends React.PureComponent {
        render() {
            const {status} = this.props;
            return (
            	<Component
                   	{...statusMap[status]}
                />
            )
        }
    }
}
复制代码

这里简单写了一些逻辑代码,可以看到将状态和行为分离之后,业务组件里面的逻辑变的非常清晰,增加状态需要修改的地方也更加方便。如果你的业务架构中使用了Redux,它可以帮助你将所有的状态转移都抽到业务代码之外,保持业务代码和受控组件的纯净度。

其实个人认为,在很多时候,代码不需要非常精炼,因为多几十行代码并不会带来很大的性能损失,但是杂乱的代码肯定会导致以后维护的时候非常高的回归成本。

总结一哈

和上篇文章不一样的地方在于,这一篇更贴近实际工作中的业务场景。在第一次实现这种复杂逻辑场景的时候,我并没有觉得这是一件需要思考的事情,但是当策划修改了一个地方的需求的时候,我的老阔开始痛了。

于是在第二次接受到这种需求的时候,花了很长时间来理顺业务的逻辑,然后画图,实现,这样一步步下来,无论需求如何变更,都可以愉快地在排期的时候多申请两天,然后快速改完,撸两天自己的兴趣。

所以,状态机并不是多么遥不可及的理论,在实际业务中可以很容易将其结合,然后提升自己的开发和迭代效率的。也可以让自己少掉许多头发哦!!

关注下面的标签,发现更多相似文章
评论