基于有限状态机的交互组件设计与实现

基于有限状态机的交互组件设计与实现

有限状态机(FSM)()是设计和实现事件驱动程序内复杂行为组织原则的有力工具。

早在2007年,IBM的工程师就提出在在JAVASCRIPT中使用有限状态机来实现组件的方法,原文地址如下:

《JavaScript 中的有限状态机》

现在结合KISSY等现代JS库和框架提供的强大的自定义事件的功能,我们可以利用有限状态机设计出代码层次清晰,结构优雅的前端交互组件。

今天,我们会通过设计并实现一个下拉选择(模拟select)组件来一步步说明如何利用FSM和KISSY来设计和实现一个有复杂行为的交互组件。

我们的工作会分成三个步骤来进行:

  • 第一步:设计组件状态,用户行为和组件行为
  • 第二步:通过代码来描述设计出来的内容
  • 第三步:实现一个有限状态机让组件工作起来

第一步:设计阶段

首先,我们需要确定组件的状态和状态间的转换关系

通过对组件可能会发生的行为进行研究,我们为组件设计了以下三个状态:

1.收起状态(fold):

www.zeeklog.com  - 基于有限状态机的交互组件设计与实现

组件的初始状态,用户可能会进行以下操作:  展开下拉框(unfoldmenu)转移到展开状态(unfold)

2.展开状态(unfold):

www.zeeklog.com  - 基于有限状态机的交互组件设计与实现

用户展开下拉框的状态,用户可能会进行以下操作:  收起下拉框(foldmenu)转移到收起状态(fold) 鼠标经过选项(overitem)转移到高亮状态(highlight)

3.高亮状态(highlight):

www.zeeklog.com  - 基于有限状态机的交互组件设计与实现

鼠标经过选项时,高亮经过的选项,用户可能会进行以下操作:  收起下拉框(foldmenu)转移到收起状态(fold) 点击选项(clickitem)转移到收起状态(fold) 鼠标经过选项(overitem)转移到高亮状态(highlight)

以上就是这个小组件可能会有的三种状态,用一个状态转换图来表示如下:

www.zeeklog.com  - 基于有限状态机的交互组件设计与实现
  • 在状态描述中包含了触发状态发生转移的动作(事件)
  • 可以很明显的看出这些事件并不是浏览器中原生的事件。
  • 这里,我们使用自定义事件来描述用户的行为,这样我们可以使得用户行为和组件行为的逻辑完全分离,代码将会更容易理解和维护。

定义用户行为:

在这个组件里,我们有以下四种用户行为:  展开下拉框(unfoldmenu):鼠标点击橙色区域时触发 收起下拉框(foldmenu):鼠标离开组件区域达到2秒,点击橙色区域,点击组 件外部区域 点击选项(clickitem):点击下拉框中的某个选项 鼠标经过选项(overitem):鼠标经过下拉框中的某个选项

定义组件行为:

在状态转移的过程中,组件本身会有很多动作,如显示下拉框等,我们接下来在上面的状态图中加入转移过程中组件的动作

www.zeeklog.com  - 基于有限状态机的交互组件设计与实现

fold():收起下拉框 unfold():展开下拉框 highlightItem():高亮某个选项 selectItem():选中某个选项,并把值填充到橘黄色区域

第二步:实现阶段(基于KISSY实现)  全局变量: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,        select = _this.select;        D.html(select,_this.text);    }, unfold:function(){        var _this = this,        slideBox = _this.slideBox;        if(!_this.isFold)return;        _this.isFold = false;        D.show(slideBox);    }, fold:function(){        var _this = this,        options = _this.options,        slideBox = _this.slideBox;        if(_this.isFold)return;        D.removeClass(options,"hover");        _this.isFold = true;        D.hide(slideBox);    }, highlightItem:function(curItem){        var _this = this,        options = _this.options;        D.removeClass(options,"hover");        D.addClass(curItem,"hover");    }, selectItem:function(curItem){        var _this = this,        value = D.attr(curItem,"data-value"),        text = D.attr(curItem,"data-text");        _this.value = value;        _this.text = text;        _this.setText()        _this.fold();        _this.fire("select",{            value:value,            text:text        });    }});

第三步:实现有限状态机(基于KISSY实现)

前面我们定义了组件的状态,用户行为,以及组件本身的动作,

接下来我们来实现一个有限状态机(FSM),让整个组件工作起来。

通过上面实现的代码,我们可以看出FSM的输入有以下三个:

  • 初始状态
  • 状态描述对象
  • 用户行为描述对象

代码结构如下:  initState:"fold",states:{    //收起(初始状态) "fold":{    },    //展开状态 "unfold":{    },    //高亮状态 "highlight":{    }}, events:{    "unfoldmenu":function(fn){    },    "foldmenu":function(fn){    },    "overitem":function(fn){    },    "clickitem":function(fn){    }}

FSM需要2个功能:

  • 将用户行为与自定义事件相关联(defineEvents)
  • 在用户行为发生时(即触发自定义事件时),根据状态描述对象来转移状态(handleEvents)

代码如下:  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,        events = 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模式设计和实现交互组件,可以获得以下特性:

  • 交互逻辑清晰
  • 用户行为和组件行为完全分离,代码具有良好的分层结构
  • 对设计具有良好的纠错特性,当设计上对状态和状态的转移有遗漏时,在实现阶段很容易流程出现走不通的情况,可以促进交互设计对细节的补充。

源码:


http://ued.taobao.org/blog/2012/10/fsm/

Read more

深入理解 Proxy 和 Object.defineProperty

在JavaScript中,对象是一种核心的数据结构,而对对象的操作也是开发中经常遇到的任务。在这个过程中,我们经常会使用到两个重要的特性:Proxy和Object.defineProperty。这两者都允许我们在对象上进行拦截和自定义操作,但它们在实现方式、应用场景和灵活性等方面存在一些显著的区别。本文将深入比较Proxy和Object.defineProperty,包括它们的基本概念、使用示例以及适用场景,以帮助读者更好地理解和运用这两个特性。 1. Object.defineProperty 1.1 基本概念 Object.defineProperty 是 ECMAScript 5 引入的一个方法,用于直接在对象上定义新属性或修改已有属性。它的基本语法如下: javascript 代码解读复制代码Object.defineProperty(obj, prop, descriptor); 其中,obj是目标对象,prop是要定义或修改的属性名,descriptor是一个描述符对象,用于定义属性的特性。 1.2 使用示例 javascript 代码解读复制代码//

By Ne0inhk

Proxy 和 Object.defineProperty 的区别

Proxy 和 Object.defineProperty 是 JavaScript 中两个不同的特性,它们的作用也不完全相同。 Object.defineProperty 允许你在一个对象上定义一个新属性或者修改一个已有属性。通过这个方法你可以精确地定义属性的特征,比如它是否可写、可枚举、可配置等。该方法的使用场景通常是需要在一个对象上创建一个属性,然后控制这个属性的行为。 Proxy 也可以用来代理一个对象,但是相比于 Object.defineProperty,它提供了更加强大的功能。使用 Proxy 可以截获并重定义对象的基本操作,比如访问属性、赋值、函数调用等等。在这些操作被执行之前,可以通过拦截器函数对这些操作进行拦截和修改。因此,通过 Proxy,你可以完全重写一个对象的默认行为。该方法的使用场景通常是需要对一个对象的行为进行定制化,或者需要在对象上添加额外的功能。 对比 以下是 Proxy 和 Object.defineProperty 的一些区别对比: 方面ProxyObject.defineProperty语法使用 new Proxy(target,

By Ne0inhk