JavaScript中的状态模式详解
作者:橘猫吃不胖~
1 什么是状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
比如说这样一个场景:
- 有一个电灯,电灯上面只有一个开关。
- 当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。
- 同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
我们用代码来描述上面的场景:
// 定义一个Light类 var Light = function () { this.state = "off"; // 给电灯设置初始状态 off this.button = null; // 电灯开关按钮 }; // 在页面中创建一个真实的button节点 Light.prototype.init = function () { var button = document.createElement("button"), // 创建一个开关按钮 self = this; button.innerHTML = "开关"; this.button = document.body.appendChild(button); // 开关被按下的事件 this.button.onclick = function () { self.buttonWasPressed(); }; }; // 开关被按下的行为 Light.prototype.buttonWasPressed = function () { // 如果当前是关灯状态,按下开关表示开灯 if (this.state === "off") { console.log("开灯"); this.state = "on"; } else if (this.state === "on") { // 如果当前是开灯状态,按下开关表示关灯 console.log("关灯"); this.state = "off"; } }; var light = new Light(); light.init();
但是灯的种类是多种多样的,另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯,现在我们改造上面的代码来完成这种新型电灯的制造:
Light.prototype.buttonWasPressed = function () { if (this.state === "off") { console.log("弱光"); this.state = "weakLight"; } else if (this.state === "weakLight") { console.log("强光"); this.state = "strongLight"; } else if (this.state === "strongLight") { console.log("关灯"); this.state = "off"; } };
在上面的代码中,存在一些很明显的缺点:
buttonWasPressed
方法违反开放—封闭原则,每次新增或者修改灯光的状态,都需要改动buttonWasPressed
方法中的代码,这使其成为了一个非常不稳定的方法- 所有跟状态有关的行为,都被封装在
buttonWasPressed
方法里,如果这个电灯又增加了其他光的种类,那这个方法会越来越庞大 - 状态的切换不明显,仅仅表现为改变
state
,容易漏掉某些状态 - 状态之间的切换关系,是靠
if
、else
语句,增加或者修改一个状态可能需要改变若干个操作,这使代码难以阅读和维护
2 使用状态模式改造电灯程序
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button
被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
同时我们还可以把状态的切换规则事先分布在状态类中, 这样就有效地消除了原本存在的
大量条件分支语句,代码如下:
// OffLightState: var OffLightState = function (light) { this.light = light; }; OffLightState.prototype.buttonWasPressed = function () { console.log("弱光"); // offLightState 对应的行为 this.light.setState(this.light.weakLightState); // 切换状态到 weakLightState }; // WeakLightState: var WeakLightState = function (light) { this.light = light; }; WeakLightState.prototype.buttonWasPressed = function () { console.log("强光"); // weakLightState 对应的行为 this.light.setState(this.light.strongLightState); // 切换状态到 strongLightState }; // StrongLightState: var StrongLightState = function (light) { this.light = light; }; StrongLightState.prototype.buttonWasPressed = function () { console.log("关灯"); // strongLightState 对应的行为 this.light.setState(this.light.offLightState); // 切换状态到 offLightState }; // 改写Light类,在Light类中为每个状态类都创建一个状态对象,可以很明显的看到灯的种类 var Light = function () { this.offLightState = new OffLightState(this); this.weakLightState = new WeakLightState(this); this.strongLightState = new StrongLightState(this); this.button = null; }; // 按下按钮的事件中,将请求委托给当前持有的状态对象去执行 Light.prototype.init = function () { var button = document.createElement("button"), // 创建button self = this; this.button = document.body.appendChild(button); this.button.innerHTML = "开关"; // 设置当前状态 this.currState = this.offLightState; this.button.onclick = function () { self.currState.buttonWasPressed(); }; }; // 切换light对象的状态 Light.prototype.setState = function (newState) { this.currState = newState; }; var light = new Light(); light.init();
3 缺少抽象类的变通方式
在上面的代码中,在状态类中将定义一些共同的行为方法,Context
最终会将请求委托给状态对象的这些方法,在这个例子里这个方法就是buttonWasPressed
。无论增加了多少种状态类,它们都必须实现buttonWasPressed
方法。
所以使用状态模式的时候要格外小心,如果我们编写一个状态子类时,忘记了给这个状态子类实现buttonWasPressed
方法,则会在状态切换的时候抛出异常,因为Context
总是把请求委托给状态对象的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("关灯"); this.light.setState(this.light.offLightState); };
4 示例:文件上传
4.1 场景描述
例如,控制文件上传需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件
- 当文件在扫描状态中,不能进行任何操作,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的
md5
值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态 - 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传
- 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件
假设我们使用一个插件对象帮助我们完成上传工作:
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; })();
上传是一个异步的过程,所以控件会不停地调用全局函数window.external.upload
,来通知目前的上传进度,控件会把当前的文件状态作为参数state
塞进window.external.upload
,在此例中该函数负责打印一些log
:
window.external.upload = function (state) { console.log(state); // 可能为 sign、uploading、done、error };
4.2 代码过程
首先定义Upload
类,在构造函数中为每种状态子类都创建一个实例对象:
var Upload = function (fileName) { this.plugin = plugin; this.fileName = fileName; this.button1 = null; this.button2 = null; this.signState = new SignState(this); // 设置初始状态为 waiting this.uploadingState = new UploadingState(); // 上传中 this.pauseState = new PauseState(this); // 暂停 this.doneState = new DoneState(this); // 上传完成 this.errorState = new ErrorState(this); // 上传错误 this.currState = this.signState; // 设置当前状态 };
创建两个按钮,一个控制文件暂停和继续上传,一个用于删除文件:
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
并不做任何具体的操作,而是把请求委托给当前的状态类来执行:
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 = "已暂停,点击继续上传"; 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); };
再接下来是编写各个状态类的实现:
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) { // 插件调用 JavaScript 的方法 uploadObj[state](); }; window.external.upload("sign"); // 文件开始扫描 setTimeout(function () { window.external.upload("uploading"); // 1 秒后开始上传 }, 1000); setTimeout(function () { window.external.upload("done"); // 5 秒后上传完成 }, 5000);
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。