[TOC]
# 狀態(tài)模式
狀態(tài)模式的關(guān)鍵是區(qū)分事物內(nèi)部的狀態(tài),事物內(nèi)部狀態(tài)的改變往往會(huì)帶來事物的行為改變。
## 初識(shí)狀態(tài)模式
狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨(dú)的類,跟此種狀態(tài)有關(guān)的行為都被封裝在這個(gè)類的內(nèi)部,只需要在上下文中,把某個(gè)請(qǐng)求委托給當(dāng)前的狀態(tài)對(duì)象即可,該狀態(tài)對(duì)象會(huì)福州渲染它自身的行為。
```javascript
// offLightState
var OffLightState = function(light){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log('弱光') // offLightState 對(duì)應(yīng)的行為
this.light.setState(this.light.weekLightState); // 切換狀態(tài)到 weekLightState
};
// WeekLightState
var WeekLightState = function(light){
this.light = light;
};
WeekLightState.prototype.buttonWasPressed = function(){
console.log('強(qiáng)光')
this.light.setState(this.light.strongLightState);
};
// StrongLightState
var StrongLightState = function(light){
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
console.log('關(guān)燈')
this.light.setState(this.light.offLightState);
};
// Light 類
var Light = function(){
this.offLightState = new OffLightState(this);
this.weekLightState = new WeekLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button');
var self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開關(guān)';
this.currState = this.offLightState;
this.button.onclick = function(){
self.currState.buttonWasPressed();
};
};
Light.prototype.setState = function(newState){
this.currState = newState;
};
var light = new Light();
light.init();
```
狀態(tài)模式可以使每一種狀態(tài)和它對(duì)應(yīng)的行為之間的關(guān)系局部化,這些行為被分散和封裝在各自對(duì)應(yīng)的狀態(tài)類中。
## 狀態(tài)模式的定義
GOF中定義:*允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)改變時(shí)改變它的行為,對(duì)象開起來似乎改變了它的類*。
- 將狀態(tài)封裝成獨(dú)立的類,并將請(qǐng)求委托給當(dāng)前的狀態(tài)對(duì)象,當(dāng)對(duì)象的內(nèi)部狀態(tài)改變時(shí),會(huì)帶來不同的行為變化。
- 使用的對(duì)象,在不同的狀態(tài)下具有截然不同的行為,這個(gè)對(duì)象看起來是從不同的類中實(shí)例化而來的,實(shí)際上是使用了委托的效果。
## 狀態(tài)模式的通用結(jié)構(gòu)
首先定義了`Ligth`類,`Light`類在這里也被稱為上下文(`Context`)。隨后在`Light`的構(gòu)造函數(shù)中,我們要?jiǎng)?chuàng)建每一個(gè)狀態(tài)類的實(shí)例對(duì)象,`Context`將持有這些狀態(tài)對(duì)象的引用,以便把請(qǐng)求委托給狀態(tài)對(duì)象。
```javascript
// Light 類
var Light = function(){
this.offLightState = new OffLightState(this);
this.weekLightState = new WeekLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button');
var self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開關(guān)';
this.currState = this.offLightState;
this.button.onclick = function(){
self.currState.buttonWasPressed();
};
};
```
最后再編寫各種狀態(tài)類,`ligth`對(duì)象被傳入狀態(tài)類的構(gòu)造函數(shù),狀態(tài)對(duì)象也需要持有`ligth`對(duì)象的引用,以便調(diào)用`ligth`中的方法或者直接操作`light`對(duì)象
```javascript
// offLightState
var OffLightState = function(light){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log('弱光') // offLightState 對(duì)應(yīng)的行為
this.light.setState(this.light.weekLightState); // 切換狀態(tài)到 weekLightState
};
```
## 缺少抽象類的變通方式
與模板方法模式中相同,讓抽象父類的抽象方法直接拋出一個(gè)異常。
```javascript
var State = function(){};
State.prototype.buttonWasPressed = function(){
throw new Error('父類的 buttonWasPressed 方法必須被重寫');
};
var SuperStrongLigthState = function(light){
this.light = light;
};
SuperStrongLigthState.prototype = new State(); // 繼承抽象父類
SuperStrongLigthState.prototype.buttonWasPressed = function(){
....
};
```
## 另一個(gè)狀態(tài)模式示例 -- 文件上傳
第一步提供`window.external.upload`函數(shù),在頁面中模擬上傳插件
```javascript
window.external.upload = function(state){
console.log(state) // 可能是sign,uploading,done,error
};
var plugin = (function(){
var plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txfn-webkit';
plugin.sign = function(){
...
};
plugin.pause = function(){
...
};
plugin.uploading = function(){
...
};
plugin.del = function(){
...
};
plugin.done = function(){
...
};
document.body.appendChild(plugin);
return plugin;
})();
```
第二步編寫`Upload`函數(shù)
```javascript
var Upload = function(filename){
this.plugin = plugin;
this.fileName = filename;
this.button1 = null;
this.button2 = null;
this.signState = new SignState(this); // 設(shè)置初始狀態(tài)為waiting
this.uploadingState = new UploadingState(this);
this.pauseState = new PauseState(this);
this.doneState = new DoneState(this);
this.errorState = new ErrorState(this);
this.currState = this.signState; // 設(shè)置當(dāng)前狀態(tài)
}
```
第三步創(chuàng)建`Upload.prototype.init`
```javascript
Upload.prototype.init = function(){
var that = this;
this.dom = document.createElement(div);
this.dom.innerHTML =
'<span>文件名稱' + this.fileName + '</span>\
<button data-action="button1">掃描中</button>\
<button data-action="button2">刪除</button>';
document.body.appendChild(this.dom);
this.button1 = this.dom.querySelector('[data-action="button1"]');
this.button2 = this.dom.querySelector('[data-action="button2"]');
this.bindEvent();
};
```
第四步負(fù)責(zé)具體的按鈕事件實(shí)現(xiàn),在點(diǎn)擊了按鈕之后,Context 并不做任何具體的操作,而是把請(qǐng)求委托給當(dāng)前的狀態(tài)類來執(zhí)行
```javascript
Upload.prototype.bindEvent = function(){
var self = this;
this.button1.onclick = function(){
self.currState.clickHandler1();
}
this.button2.onclick = function(){
self.currState.clickHandler2();
}
};
Upload.prototype.sign = function(){
this.plugin.sign();
this.currState = this.signState;
};
Upload.prototype.uploading = function(){
this.plugin.uploading();
this.currState = this.uploadingState;
};
Upload.prototype.pause = function(){
this.plugin.pasue();
this.currState = this.pauseState;
};
Upload.prototype.done = function(){
this.plugin.done();
this.currState = this.doneState;
};
Upload.prototype.error = function(){
this.plugin.error();
this.currState = this.errorState;
};
Upload.prototype.del = function(){
this.plugin.del();
this.currState = this.delState;
};
```
第五步編寫各個(gè)狀態(tài)類的實(shí)現(xiàn)
```javascript
var StateFactory = (function(){
var State = function(){};
State.prototype.clickHanlder1 = function(){
throw new Error('子類必須重寫父類的方法');
}
State.prototype.clickHanlder2 = function(){
throw new Error('子類必須重寫父類的方法');
}
return function(param){
var F = function(uploadObj){
this.uploadObj = uploadObj;
};
F.prototype = new State();
for(var i in param){
F.prototype[i] = param[i];
}
return F;
}
})();
var SignState = StateFactory({
clickHandle1: function(){
console.log(...)
},
clickHandle2: function(){
console.log(...)
}
});
var UploadingState = StateFactory({
clickHandle1: function(){
this.uploadObj.pause();
},
clickHandle2: function(){
console.log(...)
}
});
// 其它的狀態(tài)同理
...
```
測(cè)試
```javascript
var uploadObj = new Upload('xxx');
uploadObj.init();
window.external.upload = function(state){
uploadObj[state]();
};
window.external.upload('sigin')
```
## 狀態(tài)模式的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 狀態(tài)模式定義了狀態(tài)與行為之間的關(guān)系,并將它們封裝在一個(gè)類里。通過增加新的狀態(tài)類,很容易增加新的狀態(tài)和轉(zhuǎn)換。
- 避免 Context 無限膨脹,狀態(tài)切換和邏輯被分布在狀態(tài)類中,也去掉了 Context 中原本過多的條件分支。
- 用對(duì)象代替字符串來記錄當(dāng)前狀態(tài),使得狀態(tài)的切換更加一目了然。
- Context 中的請(qǐng)求動(dòng)作和狀態(tài)類中封裝的行為可以非常容易地獨(dú)立變化而互不影響。
缺點(diǎn):
會(huì)在系統(tǒng)中定義許多的狀態(tài)類,而且系統(tǒng)中會(huì)因此增加不少對(duì)象,由于邏輯分散在狀態(tài)類中,也造成了邏輯分散的問題,無法在一個(gè)地方就看出整個(gè)狀態(tài)機(jī)的邏輯。
## 狀態(tài)模式中的性能優(yōu)化點(diǎn)
可以在僅當(dāng) state 對(duì)象被需要時(shí)才創(chuàng)建并銷毀,另一種是一開始就創(chuàng)建好所有的狀態(tài)對(duì)象,并且始終不銷毀它們。
## 狀態(tài)模式和策略模式的關(guān)系
策略模式和狀態(tài)模式的相同點(diǎn)是,它們都有一個(gè)上下文、一些策略或者狀態(tài)類,上下文把請(qǐng)求委托給這些類來執(zhí)行。
它們之間的區(qū)別是策略模式中的各個(gè)策略類之間是平等又平行的,它們之間沒有任何聯(lián)系,所以客戶必須熟知這些策略類的作用,以便客戶隨時(shí)可以主動(dòng)切換算法;而在狀態(tài)模式中,狀態(tài)和狀態(tài)對(duì)應(yīng)的行為是早已被封裝好的,狀態(tài)之間的切換也早被規(guī)定完成,“改變行為”這件事情在狀態(tài)內(nèi)部。
## JavaScript版本的狀態(tài)機(jī)
```javascript
var delegate = function(client, delegation){
return {
buttonWasPressed: function(){ // 將客戶的操作類委托給 delegation 對(duì)象
return delegation.buttonWasPressed.apply(client, argument);
}
}
};
var FSM = {
off: {
buttonWasPressed: function(){
console.log(...);
this.currState = this.onState;
}
},
on: {
buttonWasPressed: function(){
console.log(...);
this.currState = this.offState;
}
}
};
var Light = function(){
this.offState = delegate(this, FSM.off);
this.onState = delegate(this, FSM.on);
this.currState = this.offState; // 設(shè)置初始狀態(tài)
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement('button'),
self = this;
this.button = document.body.appendChild(button);
this.button.click = function(){
self.currState.buttonWasPressed();
}
};
var light = new Light();
light.init();
```
## 表驅(qū)動(dòng)的有限狀態(tài)機(jī)
可以在標(biāo)準(zhǔn)很清楚地看到下一個(gè)狀態(tài)是由當(dāng)前狀態(tài)和行為共同決定的,這樣一來,我們就可以在表中查找狀態(tài),而不必定義很多條件分支。
[https://github.com/jakesgordon/javascript-state-machine/](https://github.com/jakesgordon/javascript-state-machine/)
