JavaScript中的狀態(tài)模式詳解
1 什么是狀態(tài)模式
允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為,對象看起來似乎修改了它的類。
比如說這樣一個場景:
- 有一個電燈,電燈上面只有一個開關(guān)。
- 當(dāng)電燈開著的時候,此時按下開關(guān),電燈會切換到關(guān)閉狀態(tài);再按一次開關(guān),電燈又將被打開。
- 同一個開關(guān)按鈕,在不同的狀態(tài)下,表現(xiàn)出來的行為是不一樣的。
我們用代碼來描述上面的場景:
// 定義一個Light類 var Light = function () { this.state = "off"; // 給電燈設(shè)置初始狀態(tài) off this.button = null; // 電燈開關(guān)按鈕 }; // 在頁面中創(chuàng)建一個真實的button節(jié)點 Light.prototype.init = function () { var button = document.createElement("button"), // 創(chuàng)建一個開關(guān)按鈕 self = this; button.innerHTML = "開關(guān)"; this.button = document.body.appendChild(button); // 開關(guān)被按下的事件 this.button.onclick = function () { self.buttonWasPressed(); }; }; // 開關(guān)被按下的行為 Light.prototype.buttonWasPressed = function () { // 如果當(dāng)前是關(guān)燈狀態(tài),按下開關(guān)表示開燈 if (this.state === "off") { console.log("開燈"); this.state = "on"; } else if (this.state === "on") { // 如果當(dāng)前是開燈狀態(tài),按下開關(guān)表示關(guān)燈 console.log("關(guān)燈"); this.state = "off"; } }; var light = new Light(); light.init();
但是燈的種類是多種多樣的,另外一種電燈,這種電燈也只有一個開關(guān),但它的表現(xiàn)是:第一次按下打開弱光,第二次按下打開強(qiáng)光,第三次才是關(guān)閉電燈,現(xiàn)在我們改造上面的代碼來完成這種新型電燈的制造:
Light.prototype.buttonWasPressed = function () { if (this.state === "off") { console.log("弱光"); this.state = "weakLight"; } else if (this.state === "weakLight") { console.log("強(qiáng)光"); this.state = "strongLight"; } else if (this.state === "strongLight") { console.log("關(guān)燈"); this.state = "off"; } };
在上面的代碼中,存在一些很明顯的缺點:
buttonWasPressed
方法違反開放—封閉原則,每次新增或者修改燈光的狀態(tài),都需要改動buttonWasPressed
方法中的代碼,這使其成為了一個非常不穩(wěn)定的方法- 所有跟狀態(tài)有關(guān)的行為,都被封裝在
buttonWasPressed
方法里,如果這個電燈又增加了其他光的種類,那這個方法會越來越龐大 - 狀態(tài)的切換不明顯,僅僅表現(xiàn)為改變
state
,容易漏掉某些狀態(tài) - 狀態(tài)之間的切換關(guān)系,是靠
if
、else
語句,增加或者修改一個狀態(tài)可能需要改變?nèi)舾蓚€操作,這使代碼難以閱讀和維護(hù)
2 使用狀態(tài)模式改造電燈程序
狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨(dú)的類,跟此種狀態(tài)有關(guān)的行為都被封裝在這個類的內(nèi)部,所以button
被按下的的時候,只需要在上下文中,把這個請求委托給當(dāng)前的狀態(tài)對象即可,該狀態(tài)對象會負(fù)責(zé)渲染它自身的行為。
同時我們還可以把狀態(tài)的切換規(guī)則事先分布在狀態(tài)類中, 這樣就有效地消除了原本存在的
大量條件分支語句,代碼如下:
// OffLightState: var OffLightState = function (light) { this.light = light; }; OffLightState.prototype.buttonWasPressed = function () { console.log("弱光"); // offLightState 對應(yīng)的行為 this.light.setState(this.light.weakLightState); // 切換狀態(tài)到 weakLightState }; // WeakLightState: var WeakLightState = function (light) { this.light = light; }; WeakLightState.prototype.buttonWasPressed = function () { console.log("強(qiáng)光"); // weakLightState 對應(yīng)的行為 this.light.setState(this.light.strongLightState); // 切換狀態(tài)到 strongLightState }; // StrongLightState: var StrongLightState = function (light) { this.light = light; }; StrongLightState.prototype.buttonWasPressed = function () { console.log("關(guān)燈"); // strongLightState 對應(yīng)的行為 this.light.setState(this.light.offLightState); // 切換狀態(tài)到 offLightState }; // 改寫Light類,在Light類中為每個狀態(tài)類都創(chuàng)建一個狀態(tài)對象,可以很明顯的看到燈的種類 var Light = function () { this.offLightState = new OffLightState(this); this.weakLightState = new WeakLightState(this); this.strongLightState = new StrongLightState(this); this.button = null; }; // 按下按鈕的事件中,將請求委托給當(dāng)前持有的狀態(tài)對象去執(zhí)行 Light.prototype.init = function () { var button = document.createElement("button"), // 創(chuàng)建button self = this; this.button = document.body.appendChild(button); this.button.innerHTML = "開關(guān)"; // 設(shè)置當(dāng)前狀態(tài) this.currState = this.offLightState; this.button.onclick = function () { self.currState.buttonWasPressed(); }; }; // 切換light對象的狀態(tài) Light.prototype.setState = function (newState) { this.currState = newState; }; var light = new Light(); light.init();
3 缺少抽象類的變通方式
在上面的代碼中,在狀態(tài)類中將定義一些共同的行為方法,Context
最終會將請求委托給狀態(tài)對象的這些方法,在這個例子里這個方法就是buttonWasPressed
。無論增加了多少種狀態(tài)類,它們都必須實現(xiàn)buttonWasPressed
方法。
所以使用狀態(tài)模式的時候要格外小心,如果我們編寫一個狀態(tài)子類時,忘記了給這個狀態(tài)子類實現(xiàn)buttonWasPressed
方法,則會在狀態(tài)切換的時候拋出異常,因為Context
總是把請求委托給狀態(tài)對象的buttonWasPressed
方法。
因此我們讓抽象父類的抽象方法直接拋出一個異常:
var State = function () {}; State.prototype.buttonWasPressed = function () { throw new Error("父類的 buttonWasPressed 方法必須被重寫"); }; var SuperStrongLightState = function (light) { this.light = light; }; SuperStrongLightState.prototype = new State(); // 繼承抽象父類 SuperStrongLightState.prototype.buttonWasPressed = function () { // 重寫 buttonWasPressed 方法 console.log("關(guān)燈"); this.light.setState(this.light.offLightState); };
4 示例:文件上傳
4.1 場景描述
例如,控制文件上傳需要兩個節(jié)點按鈕,第一個用于暫停和繼續(xù)上傳,第二個用于刪除文件
- 當(dāng)文件在掃描狀態(tài)中,不能進(jìn)行任何操作,既不能暫停也不能刪除文件,只能等待掃描完成。掃描完成之后,根據(jù)文件的
md5
值判斷,若確認(rèn)該文件已經(jīng)存在于服務(wù)器,則直接跳到上傳完成狀態(tài)。如果該文件的大小超過允許上傳的最大值,或者該文件已經(jīng)損壞,則跳往上傳失敗狀態(tài)。剩下的情況下才進(jìn)入上傳中狀態(tài) - 上傳過程中可以點擊暫停按鈕來暫停上傳,暫停后點擊同一個按鈕會繼續(xù)上傳
- 掃描和上傳過程中,點擊刪除按鈕無效,只有在暫停、上傳完成、上傳失敗之后,才能刪除文件
假設(shè)我們使用一個插件對象幫助我們完成上傳工作:
var plugin = (function () { var plugin = document.createElement("embed"); plugin.style.display = "none"; plugin.type = "application/txftn-webkit"; plugin.sign = function () { console.log("開始文件掃描"); }; plugin.pause = function () { console.log("暫停文件上傳"); }; plugin.uploading = function () { console.log("開始文件上傳"); }; plugin.del = function () { console.log("刪除文件上傳"); }; plugin.done = function () { console.log("文件上傳完成"); }; document.body.appendChild(plugin); return plugin; })();
上傳是一個異步的過程,所以控件會不停地調(diào)用全局函數(shù)window.external.upload
,來通知目前的上傳進(jìn)度,控件會把當(dāng)前的文件狀態(tài)作為參數(shù)state
塞進(jìn)window.external.upload
,在此例中該函數(shù)負(fù)責(zé)打印一些log
:
window.external.upload = function (state) { console.log(state); // 可能為 sign、uploading、done、error };
4.2 代碼過程
首先定義Upload
類,在構(gòu)造函數(shù)中為每種狀態(tài)子類都創(chuàng)建一個實例對象:
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.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)建兩個按鈕,一個控制文件暫停和繼續(xù)上傳,一個用于刪除文件:
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(); };
為兩個按鈕分別綁定點擊事件,在點擊了按鈕之后,Context
并不做任何具體的操作,而是把請求委托給當(dāng)前的狀態(tài)類來執(zhí)行:
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.button1.innerHTML = "正在上傳,點擊暫停"; this.plugin.uploading(); this.currState = this.uploadingState; }; // 暫停 Upload.prototype.pause = function () { this.button1.innerHTML = "已暫停,點擊繼續(xù)上傳"; this.plugin.pause(); this.currState = this.pauseState; }; // 上傳成功 Upload.prototype.done = function () { this.button1.innerHTML = "上傳完成"; this.plugin.done(); this.currState = this.doneState; }; // 上傳失敗 Upload.prototype.error = function () { this.button1.innerHTML = "上傳失敗"; this.currState = this.errorState; }; // 刪除 Upload.prototype.del = function () { this.plugin.del(); this.dom.parentNode.removeChild(this.dom); };
再接下來是編寫各個狀態(tài)類的實現(xiàn):
var StateFactory = (function () { var State = function () {}; State.prototype.clickHandler1 = function () { throw new Error("子類必須重寫父類的 clickHandler1 方法"); }; State.prototype.clickHandler2 = function () { throw new Error("子類必須重寫父類的 clickHandler2 方法"); }; 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({ clickHandler1: function () { console.log("掃描中,點擊無效..."); }, clickHandler2: function () { console.log("文件正在上傳中,不能刪除"); }, }); var UploadingState = StateFactory({ clickHandler1: function () { this.uploadObj.pause(); }, clickHandler2: function () { console.log("文件正在上傳中,不能刪除"); }, }); var PauseState = StateFactory({ clickHandler1: function () { this.uploadObj.uploading(); }, clickHandler2: function () { this.uploadObj.del(); }, }); var DoneState = StateFactory({ clickHandler1: function () { console.log("文件已完成上傳, 點擊無效"); }, clickHandler2: function () { this.uploadObj.del(); }, }); var ErrorState = StateFactory({ clickHandler1: function () { console.log("文件上傳失敗, 點擊無效"); }, clickHandler2: function () { this.uploadObj.del(); }, });
測試一下:
var uploadObj = new Upload("AAAAAAAAA"); uploadObj.init(); window.external.upload = function (state) { // 插件調(diào)用 JavaScript 的方法 uploadObj[state](); }; window.external.upload("sign"); // 文件開始掃描 setTimeout(function () { window.external.upload("uploading"); // 1 秒后開始上傳 }, 1000); setTimeout(function () { window.external.upload("done"); // 5 秒后上傳完成 }, 5000);
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Bootstrap模塊dropdown實現(xiàn)下拉框響應(yīng)
這篇文章主要為大家詳細(xì)介紹了Bootstrap下拉框模塊dropdown實現(xiàn)下拉框響應(yīng),感興趣的朋友可以參考一下2016-05-05基于javascript實現(xiàn)仿百度輸入框自動匹配功能
這篇文章主要介紹了基于javascript實現(xiàn)仿百度輸入框自動匹配功能的相關(guān)資料,需要的朋友可以參考下2016-01-01Javascript 創(chuàng)建類并動態(tài)添加屬性及方法的簡單實現(xiàn)
下面小編就為大家?guī)硪黄狫avascript 創(chuàng)建類并動態(tài)添加屬性及方法的簡單實現(xiàn)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10