首先声明一点,这个模式是我目前见过最好用(本人观点),但是也是最难理解的一个(本人观点)。 所以大家需要做好心理准备,如果,对这个模式没有特别强烈的需求,比如: 我有一个Button,我按次数点击它,他会触发不同的状态 等等这样的,可以学习一下其他的模式。但是!!! 如果你看了我这篇文章,被我前面说的话吓到了,那么就继续往下看,其实,状态模式是最好用,也是最容易掌握的一个。
大话状态模式
上面已经提到了,状态模式其实就是,一个事物的内部状态的改变,产生不同的行为。 要注意这里的
"一个","状态","行为". 因为这3个词是状态模式中最重要的3个概念。
同样,举个栗子
大家家里都有空调吧,remoter应该都用过。 首先我们拿一个简单的使唤。 就拿 on/off 键吧。首先,遥控器会记录当前的状态(假设他会这样),如果是on, 当你点击 他会发出off的信号,如果你是off,则会发出on的信号.我们用程序说明一下.
var switches = (function(){
var state = "off";
return function(){
if(state === "off"){
console.log("打开空调");
state = "on";
}else if(state === "on"){
console.log("关闭空调");
state = "off";
}
}
})();
document.querySelector(".switch").addEventListener("click",function(){
switches(); //模仿你打开/关闭空调的状态
},false)
很简单吧,一个闭包+一个变量 就可以构成一个 状态机,是不是超级神奇呢?
恩,看到这里,聪明的人会,会心一笑,然后继续往下看。
是个屁。
想想,如果空调开关要是都只有两种状态,尼玛谁用啊!!!
满是turn on/off. 就跟写2进制一样。实话说,哥汇编学的差,所以也十分不愿意直视二进制,你让我使唤开关跟写汇编似的,操。 0001是开,0000是关,0010是加热模式,0011是制冷模式。
所以,海尔,美的考虑到国情,制造了比较人性化的remoter。
现在,如果我们使用上面那种模式来写,切换模式的switch。
var switches = (function(){ //auto->hot->cold->wind->dry->auto
var state = "auto";
return function(){
if(state === "auto"){
console.log("制热");
state = "hot";
}else if(state === "hot"){
console.log("制冷");
state = "cold";
}else if(state === "cold"){
console.log("送风");
state = "wind";
}else if(state === "wind"){
console.log("除湿");
state = "dry";
}else if(state === "dry"){
console.log("自动");
state = "auto";
}
}
})();
document.querySelector(".switch").addEventListener("click",function(){
switches(); //模仿你切换空调的模式
},false);
呵呵呵~ 功能是实现了,不过代码,又被if语句给rape的。 性能的强奸犯,阅读的杀手 恐怕就算if语句了. 所以,为了不犯罪,我们需要优化我们的状态模式。
高级状态模式
其实,这个状态模式的写法和命令模式有着异曲同工的妙处。即,中间有个状态仓库,然后分别将命令转发给对应的执行类。
总结一下。 高级状态模式需要有,状态仓库,状态类,状态执行者,这3个要点。 对应着我们的,”一个“,"状态","行为". 一个仓库,不同的状态,不同的执行。
just do it.
//定义状态
var Auto= function(button){
this.turn = button;
}
Auto.prototype.press= function(){
console.log('制热');
this.turn.setState("hot");
}
var Hot = function(button){
this.turn = button;
}
Hot.prototype.press= function(){
console.log('制冷');
this.turn.setState("cold");
}
var Cold = function(button){
this.turn = button;
}
Cold.prototype.press= function(){
console.log('送风');
this.turn.setState("wind");
}
var Wind = function(button){
this.turn = button;
}
Wind.prototype.press= function(){
console.log('除湿');
this.turn.setState("dry");
}
var Dry = function(button){
this.turn = button;
}
Dry.prototype.press= function(){
console.log('自动');
this.turn.setState("auto");
}
//定义状态仓库
var Remoter = function(){
this.auto = new Auto(this);
this.hot = new Hot(this);
this.cold = new Cold(this);
this.wind = new Wind(this);
this.dry = new Dry(this);
this.state = "auto";
}
Remoter.prototype.setState = function(state){
this.state=state;
}
Remoter.prototype.press = function(){
this[this.state].press(); //执行对应状态的press
}
Remoter.prototype.init = function(){ //定义执行者
document.querySelector('.switch').addEventListener("click",()=>{
this.press();
},false);
}
new Remoter.init(); //初始化
上面那种就是一个比较模式化的写法,而且,可复用,可添加。 比上面那种的逼格不知道高到哪里去,但是,实现成本也是挺大的。考虑到这点,聪明的ECMA-262在es6中推出了状态机这个伪函数,能够帮助我们快速实现状态化。
Duang~
就是generator函数。 目前FF,edge,chrome 最新版本已经支持。不过可以使用babel进行转化.
我们使用generator进行重构.
我比较懒,我们就先实现前3个模式的转化吧。
var auto = function(){
console.log("自动");
}
var hot = function(){
console.log("制热");
}
var cold = function(){
console.log("制冷");
}
function* models(){
for(var i = 0,fn,len=arguments.length;fn = arguments[i++];){
yield fn();
if(i===len){
i = 0;
}
}
}
var exe = models(auto,hot,cold); //按照模式顺序排放
document.querySelector(".switch").addEventListener("click",function(){
exe.next();
},false);
已经没有了if来进行分支判断,效果也是蛮不错的。 关于generator的用法,还有进程控制,这些都是比较高级的用法,有兴趣的同学可以参考 阮老师的 es6讲解. 但是,推荐还是使用,一个仓库,不同状态,不同行为,这样函数对象式的写法,扩展性比较强。主要原因是因为,generator还未普及,以及设置他进程的顺序比较复杂。不过,平常本人喜欢装装逼,永远热爱新技术,所以大部分时候还是会使用generator。 总之,程序员并不是程序员,我们要有自己的核心价值观,找到自己最对的 "玛卡瑞纳",这才是我们程序员应该有的情怀。
我们仔细观察一下上面使用"类"写出来的状态模式,会发现,状态类是不是感觉可以使用享元模式优化呢?没错。因为他的方法和状态都是一致的,当然可以使用。
var obj = {
auto(){
console.log("自动")
return "hot";
},
hot(){
console.log("制热");
return "cold";
},
cold(){
console.log("制冷");
return "auto"
}
}
var State = function(){
this.state = "auto";
this.obj = obj;
}
State.prototype.next = function(){
this.state = this.obj[this.state]()
}
new State().next(); //测试通过.
这只是一种比较轻巧的方法,js,最出名的就是他的动态,无拘无束,你可以天马行空的写出你的代码(但是,必须保证的你代码不会变成 凤姐 ).
但从上面的代码可以看出,如果程序里面使用return 的话,很容易会造成你函数的逻辑复杂度,所以我们这里推荐使用一个state进行保存,将this.state传入。 当然,我们并不是当参数参入了(太low),我们使用委托的技术传入,相当于给this动态织入一个函数。这个方法就叫: apply和call. 哈哈,是不是有种感觉(怎么又是你).
var obj = {
auto(){
console.log("自动")
this.state = "hot";
},
hot(){
console.log("制热");
this.state = "cold";
},
cold(){
console.log("制冷");
this.state = "auto";
}
}
var State = function(){
this.state = "auto";
this.obj = obj;
}
State.prototype.next = function(){
this.obj[this.state].call(this);
}
new State().next();
没错,这下,我们不仅能将函数动态织入,而且可以直接改动state,这样可以给自己程序的扩展性加上一分。
当然,状态模式的写法还有很多,比如delegate函数的写法等等。 不过,找到自己的"玛卡瑞纳"才是最棒的。
上面只是一个线上的流式状态切换,并没有涉及很复杂的业务逻辑。但是,如果你在开发一个大型项目的时候,涉及的状态可谓是五花八门,还是以空调遥控器为例,比如,你切换到模式选择的时候,你的上下左右键,只能控制模式的切换,而不能控制风速大小,当你切换到风速选择模式的时候,同样不能控制其他的功能。 所以,如果按照上面那种 单线式的状态切换是不够的。 这里就引入了FsM(finite-state-machine),状态机这个概念,以及和他对应的状态表。
如下图
如果你是学机械的,那么这个状态切换的概念应该非常熟悉,在CH40161(一种自触发式芯片)中,你输入一个触发信号,他可以按照你这个触发信号逐步触发(我机械太渣,但意外的喜欢上计院). 在js中,gordon大神(有8个contributor)已经写出了这个状态库。有兴趣的同学可以看一看。
传送门: FSM。
其实,他里面最重要的就是"状态"和"状态切换"的规则。
先看一个demo:
var fsm = StateMachine.create({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' },
{ name: 'panic', from: 'yellow', to: 'red' },
{ name: 'calm', from: 'red', to: 'yellow' },
{ name: 'clear', from: 'yellow', to: 'green' }
],
callbacks: {
onpanic: function(event, from, to, msg) { alert('panic! ' + msg); },
onclear: function(event, from, to, msg) { alert('thanks to ' + msg); },
ongreen: function(event, from, to) { document.body.className = 'green'; },
onyellow: function(event, from, to) { document.body.className = 'yellow'; },
onred: function(event, from, to) { document.body.className = 'red'; },
}
});
这已经定义好了一个完整的单线式,状态切换队列。
当你触发fsm.warn(); 状态就是从green->yellow。
当你触发fsm.panic(); 状态就是从yellow->red.
...
说一下基本用法
events 里面就是你定义的状态表的规则
name: 标识,状态切换的函数名
from: 标识 为切换之前的状态
to: 标识 为切换之后的状态
callbacks 里面就是对状态和切换规则函数的定义. 这里不说的太复杂,就按照基本的讲解吧。
使用on+Name; 定义状态切换的函数
使用on+State: 定义某个状态时触发的函数
当然,还有
onbeforeevent - fired before any event
onleavestate - fired when leaving any state
onenterstate - fired when entering any state
onafterevent - fired after any event
这些比较细,这里就不做详细介绍,如果有兴趣的同学可以去github上面看一看,理解起来也不是很难。我这里介绍的我经常使用的。
所以,上面的流程就是。
使用fsm.panic() 之后。
触发顺序为: onpanic()->red();
如果你状态不对,而强行调用fsm.panic的话就会触发error函数(这里没有写). 所以,上面写的fsm 差不多已经够用了,关键看你如果组合了。 要知道,二维难度 >> 一维难度。 有一个好工具,能把你的工作量降到最低。
谈谈状态模式
说到这里,我的这篇blog大部分是介绍 一些基本原理和方法,状态模式的应用在程序设计中是非常重要的一个概念,如果你掌握了,语言只会变为你的一个工具,因为 你已经吃透了 隐藏在 语言背后的 secret. 最后还是那句话, 不要为了模式而模式,但状态模式确实是个好模式。
ending~.