本文通过实际业务例子举例,如何将状态机思想迁移并运用到实际业务开发场景中,从而帮助我们在开发中,简化复杂的多状态流转场景。
1. 实际问题
先描述一下需求背景,项目中要实现一个搜索页,搜索页的主要逻辑大致为:
- 当用户未输入时,展示快捷输入面板:
- 当用户在搜索框输入后,展示联想输入的列表:
- 当确定搜索的关键词时,包含从快捷面板或联想面板选择、搜索框输入后回车确定,展示搜索结果列表:
在对需求进行分析之后,可以总结出一个规律:根据用户的不同操作,需要呈现相应的功能。
以上场景,恰好符合有限状态机的三个特征:
- 状态机(State)的个数是有限的
- 在任一时刻,只处于一种状态
- 当满足某种触发条件(Event),就会从某种状态转移(Transition)到另一种状态,并同时执行某个动作(Action)。
那么,采用状态机的思想,将对用户不同状态流转进行描述,是不是会更加清晰呢?
2. 概念
我们先来看一下,有限状态机四个核心概念的定义:
- 状态(State):包括初始状态和事件触发后的状态,同时必须要有一个最终状态。
- 事件(Event):触发状态机从一种状态切换到另一种状态。
- 转移(Transition):表示从一个状态切换到另一个状态。引起状态迁移的事件被称为触发事件(triggering event)。
- 动作(Action):表示在进行状态转移后要执行的具体行为。
3. 在实际的场景中,如何用状态机的思想抽象?
S1:抽离状态 —— 划分用户的操作阶段,枚举存在的状态
- 输入前(初态)
- 正在输入的状态
- 输入后有结果(终态)
S2:定义动作 —— 找出转移到每种状态后对应的行为
- 展示快捷输入面板
- 展示联想输入的列表
- 展示搜索结果列表
S3:明确事件 —— 找出触发状态发生转化的条件
通过一个状态转化图,描述状态间的转化关系:
4. 如何实现?
S1 :按划分的状态,定义变量的值
searchStatus用来表示搜索页的搜索状态,即before-输入前,doing-正在输入,after-确认输入结果(包括:输入关键词后回车确认、点击联想列表、以及热门和历史规则的情况)
data() {
return {
searchStatus: 'before', // 默认首次进入该页,为输入前的状态
searchResult: '' // 记录搜索结果
}
}
复制代码
S2 :按状态以及每种状态对应的动作,进行组件拆分,并与每个状态值关联
将整个搜索页拆成了两部分:
第一部分是:在三个状态下,都共有的搜索框
<div class="keyword-search">
<search-input
ref="searchInput"
placeholder="搜索"
@debounce-input="valueChange"
@keyup="keyup"
@cancel="cancelSearch"
></search-input>
</div>
复制代码
第二部分,则通过切换不同的状态,展示在三种状态下相应的组件。包括:
- 热搜、历史搜索面板组件
在输入的初始态,应展示热搜、历史搜索面板。这里,用 this.searchStatus === 'before'
控制是否展示。
<search-option-panel
v-if="searchStatus === 'before'"
title="热搜规则"
@click="selectHotResult"
></search-option-panel>
<search-option-panel
v-if="searchStatus === 'before'"
title="搜索历史"
@click="selectKeywordOption"
@clear="clearSearchHistory"
></search-option-panel>
复制代码
- 联想输入的列表
在用户在输入框中,输入文字时,即输入中,应展示联想输入的列表,其中,包括用户输入无相关的联想词时,提示暂无内容相关的相关逻辑。这里,用 this.searchStatus === 'doing'
控制是否展示。
<div class="related-keyword-list-wrapper" v-if="searchStatus === 'doing'">
<div class="related-keyword-list" v-if="relatedKeywordList.length">
<div class="keyword-option border-bottom-1px"
v-for="(value, index) in relatedKeywordList"
:key="index"
@click="selectKeywordOption(value)">
{{value}}
</div>
</div>
<div v-else class="no-keyword-list-wrapper">
<div class="no-data-icon"></div>
<div class="no-data-tip">抱歉,未找到相关结果。</div>
<div class="no-data-suggest">可以尝试别的关键词</div>
</div>
</div>
复制代码
- 搜索结果列表
在获取用户输入内容之后,应展示搜索结果列表。这里,使用 this.searchStatus === 'after'
控制结果列表的展示。当用户手动输入(通过回车确认)、或从搜索联想列表、以及热门和历史规则中选择后,设置 searchStatus
为 after
,并将选择的搜索结果用 searchResult
存储,展示搜索结果列表。该部分的逻辑还包括用户输入无相关结果时,提示暂无内容相关的相关逻辑。
<list ref="list" v-if="searchStatus === 'after' && searchResult"></list>
复制代码
S3:根据事件触发,切换不同的状态值
(以下代码为了便于理解,只保留控制状态切换的关键逻辑)
methods: {
// 选择热搜面板中关键词
selectHotResult(val) {
this.searchStatus = 'after'
this.searchResult = val
},
// 选择历史搜索面板中关键词、选择搜索联想词
selectKeywordOption(val) {
this.searchStatus = 'after'
this.searchResult = val
},
// 输入搜索词后,回车确认
keyup($event) {
if ($event.keyCode === 13) {
this.searchStatus = 'after'
this.searchResult = this.searchValue
}
},
// 点击搜索框的取消按钮
cancelSearch() {
if (this.searchStatus === 'before') {
this.$router.go(-1)
} else {
this.searchStatus = 'before'
this.searchResult = ''
this.$refs.searchInput.clearValue()
}
},
// 在输入框输入关键字
valueChange() {
this.searchStatus = 'doing'
this.searchValue = v
this.searchResult = ''
...
}
}
watch: {
searchResult: {
handler(value) {
if (!value) {
return
}
this.searchStatus = 'after'
...
}
}
}
复制代码
总结
状态机的思想,适用于需要根据多个状态控制不同功能的业务场景。开发者只需要聚焦于在对状态的控制上,以及关心渲染每种状态相对应的行为即可,每组的状态、行为以及结果,互相独立,即使每组之间若在后期发生变化,都是互不影响的。
在实际的开发场景中,当我们遇到状态多、分支复杂的情况,可以考虑用状态机的思想来梳理逻辑,从而帮助我们更清晰、更快速地梳理业务逻辑,避免使用庞大、繁琐的条件语句。