有限状态机(FSM)(维基百科)是设计和实现事件驱动程序内复杂行为组织原则的有力工具。
早在 2007 年,IBM 的工程师就提出在在 JavaScript 中使用有限状态机来实现组件的方法,原文地址如下:
现在结合 KISSY 等现代 JS 库和框架提供的强大的自定义事件的功能,我们可以利用有限状态机设计出代码层次清晰,结构优雅的前端交互组件。
今天,我们会通过设计并实现一个下拉选择(模拟 select)组件来一步步说明如何利用 FSM 和 KISSY 来设计和实现一个有复杂行为的交互组件。
我们的工作会分成三个步骤来进行:
首先,我们需要确定组件的状态和状态间的转换关系
通过对组件可能会发生的行为进行研究,我们为组件设计了以下三个状态:
1. 收起状态(fold):
组件的初始状态,用户可能会进行以下操作:
2. 展开状态(unfold):
用户展开下拉框的状态,用户可能会进行以下操作:
3. 高亮状态(highlight):
!{](https://gw.alicdn.com/tfs/TB1cD2RQXXXXXaZXVXXXXXXXXXX-242-186.png)
鼠标经过选项时,高亮经过的选项,用户可能会进行以下操作:
以上就是这个小组件可能会有的三种状态,用一个状态转换图来表示如下:
定义用户行为:
在这个组件里,我们有以下四种用户行为:
在状态转移的过程中,组件本身会有很多动作,如显示下拉框等,我们接下来在上面的状态图中加入转移过程中组件的动作
全局变量:S=KISSY, D=S.DOM, E=S.Event
1. 描述状态
跟设计过程一样,我们需要用一个结构来描述状态的转移以及转移过程中的动作
我们在这里使用对象来描述:
fold: {
unfoldmenu: function(event) {
_this.unfold();
return 'unfold';
}
}
如上面这段代码就描述了在 fold 状态下,可以触发 unfoldmenu 这个用户行为来转移到 unfold 状态,
我们通过函数返回值的形式来通知 FSM 下一步的状态。
这样,我们就可以通过这种形式描述所有的状态,结构如下:
states: {
// 收起(初始状态)
fold: {
unfoldmenu: function(event) {
_this.unfold();
return 'unfold';
}
},
// 展开状态
unfold: {
foldmenu: function(event) {
_this.fold();
return 'fold';
},
overitem: function(event) {
_this.highlightItem(event.currentItem);
return 'highlight';
}
},
// 高亮状态
highlight: {
foldmenu: function(event) {
_this.fold();
return 'fold';
},
// 选中条目
clickitem: function(event) {
_this.selectItem(event.currentItem);
return 'fold';
},
overitem: function(event) {
_this.highlightItem(event.currentItem);
return 'highlight';
}
}
}
在定义好状态后,我们还需要设定一个初始状态:
initState: 'fold'
2. 描述用户行为
我们使用一个方法来描述用户行为,即驱动 FSM 发生状态转移的事件:
foldmenu: function(fn) {
var timeout;
E.on(_this.container, 'mouseleave', function(e) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
fn();
}, 1000);
});
E.on([_this.container, _this.slideBox], 'mouseenter',
function(e) {
if (timeout) clearTimeout(timeout);
});
E.on('body', 'click', function(e) {
var target = e.target;
if (!D.get(target, _this.container)) {
if (timeout) clearTimeout(timeout);
fn();
}
});
}
如上面这个代码就定义了 foldmenu 这个用户行为,同时,FSM 会自动将它定义为一个自定义事件,我们通过传入的回调函数 fn 来通知 FSM 触发这个事件的时机。
通过上边的例子可以看出,我们可以将一个很复杂的动作定义为一个用户行为,也可以将几个不同的动作定义为一个用户行为,将用户行为和组件的动作彻底分开。
与状态相同,我们也将所有的用户行为放在一个对象中。
events: {
unfoldmenu: function(fn) {},
foldmenu: function(fn) {},
overitem: function(fn) {},
clickitem: function(fn) {}
}
3. 描述组件行为
由于组件行为一般都包含对组件本身的一些直接操作,可以作为 API 开放给用户使用,因此我们把描述组件行为的方法放在组件的 prototype 上,这部分代码如下:
S.augment(SlideMenu, S.EventTarget, {
setText: function() {
var _this = this;
var select = _this.select;
D.html(select, _this.text);
},
unfold: function() {
var _this = this;
var slideBox = _this.slideBox;
if (!_this.isFold) return;
_this.isFold = false;
D.show(slideBox);
},
fold: function() {
var _this = this;
var options = _this.options;
var slideBox = _this.slideBox;
if (_this.isFold) return;
D.removeClass(options, 'hover');
_this.isFold = true;
D.hide(slideBox);
},
highlightItem: function(curItem) {
var _this = this;
var options = _this.options;
D.removeClass(options, 'hover');
D.addClass(curItem, 'hover');
},
selectItem: function(curItem) {
var _this = this;
var value = D.attr(curItem, 'data-value');
var text = D.attr(curItem, 'data-text');
_this.value = value;
_this.text = text;
_this.setText()
_this.fold();
_this.fire('select', {
value: value,
text: text
});
}
});
前面我们定义了组件的状态,用户行为,以及组件本身的动作,
接下来我们来实现一个有限状态机(FSM),让整个组件工作起来。通过上面实现的代码,我们可以看出 FSM 的输入有以下三个:
代码结构如下:
initState: 'fold',
states: {
// 收起(初始状态)
fold: {},
// 展开状态
unfold: {},
// 高亮状态
highlight: {}
},
events: {
unfoldmenu: function(fn) {},
foldmenu: function(fn) {},
overitem: function(fn) {},
clickitem: function(fn) {}
}
FSM 需要两个功能:
代码如下:
functionFSM(config) {
this.config = config;
this.currentState = this.config.initState;
this.nextState = null;
this.states = this.config.states;
this.events = this.config.events;
this.defineEvents();
}
var proto = {
// 事件驱动状态转换(表现层)
handleEvents: function(event) {
if (!this.currentState) return;
var actionTransitionFunction = this.states[this.currentState][event.type];
if (!actionTransitionFunction) return;
var nextState = actionTransitionFunction.call(this, event);
this.currentState = nextState;
},
// 定义事件 (行为层)
defineEvents: function() {
var _this = this;
varevents = this.events;
for (k in events) {
(function(k) {
var fn = events[k];
fn.call(_this, function(event) {
_this.fire(k, event);
});
_this.on(k, _this.handleEvents);
})(k)
}
}
}
S.augment(FSM, S.EventTarget, proto);
然后,只需要实例化一个 FSM 即可
new FSM({
initState: 'fold',
states: {...},
events: {...}
});
最后,总结一下。
使用 FSM 模式设计和实现交互组件,可以获得以下特性:
← 复杂表单应用解耦,淘宝机票订单实践 构建前端 DSL →题图:https://unsplash.com/photos/hyquz5mHdok By @Nathan Hulsey