Created
January 5, 2013 04:32
-
-
Save coodoo/4459759 to your computer and use it in GitHub Desktop.
Implemented in a way that resembles SMC (http://smc.sourceforge.net/), primarily adding two callbacks a. action()
b. guard() Changes I made are marked with "jx" in the comment.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$().on('ready', ready()); | |
function ready(){ | |
var fsm = StateMachine.create({ | |
initial: 'hungry', | |
/** | |
* 下面是依 smc 精神設計的 api,主要好處是可每組條件指定 callback,而不用 switch/case | |
* | |
* guard: 判斷是否允許執行此 transition,有時條件未滿足,就不允許,效果類似 onBeforeEvent,但可每組條件指定一個 callback | |
* | |
* action: 一個 event 觸發時,要執行的工作,效果類似 onAfterEvent(),但可每組條件指定一個 callback | |
* | |
* 適用時機: | |
* 。如果像 eat event 可能會有多組 state 共用,為了避免在 callback 內寫一大堆 switch/case,就可每組明確指定 callback | |
* 。但如果 event 不會被共用,就可用原版設計的全域 callback,通常這適用於較單純的 state machine | |
*/ | |
events: [ | |
{ name: 'eat', from: 'hungry', to: 'satisfied', action:'foo', guard:'guardFoo' }, | |
{ name: 'eat', from: 'satisfied', to: 'full', action:'bar' }, | |
{ name: 'eat', from: 'full', to: 'sick', action:'coo' }, | |
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry', action:'doo' }, | |
]}); | |
//======================================================================== | |
// | |
// guards | |
/* | |
Transition Guards | |
Conditions that must be met in order for transitions to proceed | |
*/ | |
/** | |
* @return BOOLEAN false will cancel the transition | |
*/ | |
fsm.guardFoo = function( name, from, to){ | |
console.log( 'GUARD: ', from, '→', to, ' >by "eat" event ' ); | |
return true; | |
} | |
//======================================================================== | |
// | |
// actions | |
fsm.foo = function( name, from, to, args ){ | |
console.log( 'ACTION [foo]: ', from, '→', to, ' >for event= ', name ); | |
} | |
//======================================================================== | |
// | |
// event - global | |
//before | |
fsm.onbeforeeat = function( evt, from, to, args ){ | |
console.log( ' >before event= ', evt, ' > ', from, '→' ,to, ' >args: ', args ); | |
return true; | |
} | |
//after | |
fsm.oneat = function( evt, from, to, args ){ | |
console.log( ' >after event= ', evt, ' > ', to ); | |
}; | |
//======================================================================== | |
// | |
// state - global | |
fsm.onleavehungry = function( evt, from, to, args ){ | |
console.log( args, ' leave state= ', evt, from ); | |
}; | |
fsm.onentersatisfied = function( evt, from, to, args ){ | |
console.log( args, ' enter state= ', evt, to ); | |
}; | |
fsm.onleavesatisfied = function( evt, from, to, args ){ | |
console.log( args, ' leave state= ', evt, from ); | |
}; | |
//======================================================================== | |
// | |
// runner | |
var result = fsm.eat( '1st' ); | |
console.log( 'result: ', result ); | |
// fsm.eat( '2nd' ); | |
// fsm.eat( '3rd' ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (window) { | |
var StateMachine = { | |
//--------------------------------------------------------------------------- | |
VERSION: "2.2.0", | |
//--------------------------------------------------------------------------- | |
Result: { | |
SUCCEEDED: 1, // the event transitioned successfully from one state to another | |
NOTRANSITION: 2, // the event was successfull but no state transition was necessary | |
CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback or transition guard //jx | |
ASYNC: 4 // the event is asynchronous and the caller is in control of when the transition occurs | |
}, | |
Error: { | |
INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state | |
PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending | |
INVALID_CALLBACK: 300 // caller provided callback function threw an exception | |
}, | |
WILDCARD: '*', | |
ASYNC: 'async', | |
//--------------------------------------------------------------------------- | |
create: function(cfg, target) { | |
var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false } | |
var fsm = target || cfg.target || {}; | |
var events = cfg.events || []; | |
var callbacks = cfg.callbacks || {}; | |
var map = {}; | |
var add = function(e) { | |
var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified | |
map[e.name] = map[e.name] || {}; | |
for (var n = 0 ; n < from.length ; n++) | |
//jx: 加上 action, guard 指令 | |
// allow no-op transition if 'to' is not specified | |
map[e.name][from[n]] = { | |
to: e.to || from[n], | |
action: e.action, | |
guard: e.guard }; //檢查是否允許執行此 transition, | |
}; | |
if (initial) { | |
initial.event = initial.event || 'startup'; | |
add({ name: initial.event, from: 'none', to: initial.state }); | |
} | |
for(var n = 0 ; n < events.length ; n++) | |
add(events[n]); | |
for(var name in map) { | |
if (map.hasOwnProperty(name)) | |
fsm[name] = StateMachine.buildEvent(name, map[name]); | |
} | |
for(var name in callbacks) { | |
if (callbacks.hasOwnProperty(name)) | |
fsm[name] = callbacks[name] | |
} | |
fsm.current = 'none'; | |
fsm.is = function(state) { return this.current == state; }; | |
fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } | |
fsm.cannot = function(event) { return !this.can(event); }; | |
fsm.error = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17) | |
if (initial && !initial.defer) | |
fsm[initial.event](); | |
return fsm; | |
},//end create() state | |
//=========================================================================== | |
doCallback: function(fsm, func, name, from, to, args) { | |
if (func) { | |
try { | |
return func.apply(fsm, [name, from, to].concat(args)); | |
} | |
catch(e) { | |
return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e); | |
} | |
} | |
}, | |
beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); }, | |
afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); }, | |
leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); }, | |
enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); }, | |
changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); }, | |
//jx | |
defaultAction: function( name, from, to, args ){ console.log( 'no action assigned for event = ', name );}, | |
//jx: 沒提供 guard 的話,無條件允許執行此 transition | |
defaultGuard: function( name, from, to, args ){ return true; }, | |
buildEvent: function(name, map) { | |
return function() { | |
var from = this.current; | |
var to = map[from].to || map[StateMachine.WILDCARD] || from; //jx | |
//jx | |
var action = this[map[from].action]; | |
action = action === undefined ? StateMachine.defaultAction : action; | |
//jx | |
var guard = this[map[from].guard]; | |
guard = guard === undefined ? StateMachine.defaultGuard : guard; | |
// | |
var args = Array.prototype.slice.call(arguments); // turn arguments into pure array | |
if (this.transition) | |
return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); | |
if (this.cannot(name)) | |
return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); | |
if (false === StateMachine.beforeEvent(this, name, from, to, args)) | |
return StateMachine.Result.CANCELLED; | |
//jx - 先檢查 guard condition 是否有通過,才能執行此 transition | |
if( false === guard.apply(this, [name, from, to].concat(args)) ){ | |
return StateMachine.Result.CANCELLED; | |
} | |
//jx - 執行 action callback | |
//注意:故意讓上面 beforeevent 先跑,那裏面可能有 guardian 會擋掉事件與state 轉換 | |
// action( name, from, to, args.slice() ); | |
action.apply(this, [name, from, to].concat(args)); | |
if (from === to) { | |
StateMachine.afterEvent(this, name, from, to, args); | |
return StateMachine.Result.NOTRANSITION; | |
} | |
// prepare a transition method for use EITHER lower down, | |
// or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) | |
var fsm = this; | |
this.transition = function() { | |
fsm.transition = null; // this method should only ever be called once | |
fsm.current = to; | |
StateMachine.enterState( fsm, name, from, to, args); | |
StateMachine.changeState(fsm, name, from, to, args); | |
StateMachine.afterEvent( fsm, name, from, to, args); | |
}; | |
this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22) | |
fsm.transition = null; | |
StateMachine.afterEvent(fsm, name, from, to, args); | |
} | |
; | |
var leave = StateMachine.leaveState(this, name, from, to, args); | |
if (false === leave) { | |
this.transition = null; | |
return StateMachine.Result.CANCELLED; | |
} | |
else if ("async" === leave) { | |
return StateMachine.Result.ASYNC; | |
} | |
else { | |
if (this.transition) | |
this.transition(); // in case user manually called transition() but forgot to return ASYNC | |
return StateMachine.Result.SUCCEEDED; | |
} | |
}; | |
} | |
}; // StateMachine | |
//=========================================================================== | |
if ("function" === typeof define) { | |
define(function(require) { return StateMachine; }); | |
} | |
else { | |
window.StateMachine = StateMachine; | |
} | |
}(this)); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment