一. 引子
之前做图片滚动的时候,我萌生一个想法,类似于这种属性渐变的东西,能不能抽象出来以方便以后的使用。于是开始了艰苦卓绝的编码。不过,写着写着,发现变了味道,随着逐步的抽象和封装,最后居然发现底层的需求居然是时钟+计时器这种简单的东西,于是大骂一声SHIT,哥怎么早没看出来呢!
现在,回顾过去这几天折腾的过程,我发现,整个抽象、封装的过程实际上就是一个趋近于单一职责的过程,在这个过程里,代码的逻辑越来越简单,if 语句越来越少,各模块越来越容易单独调试。
此外,冒着暴露我是水货的危险,我还是要感慨,观察者模式真的很强大,时至今日,我用的最多的模式就是它了。即便是在我的伪 MVC 实践中,也离不开观察者。
接下来,我将逐步分析我的这四个模型简单地讲解并给出使用实例,至于我提取、封装、抽象模型的过程中的一些想法,都整在最后的附录里,是无法阅读的部分。
二. 模型
Clock
Timer
Cmd
CmdQueuer
三. 试水
先通过代码简单看看,如果有这样一个HTML文件:
<html>
<head>
<style type="text/css">
body{
margin:0;padding:0;position:relative;
}
#dd1,#dd2,#dd3,#dd4{
position:absolute;
}
#dd1{
background-color:green;
left:0;top:0;
}
#dd2{
background-color:yellow;
left:200px;top:0;
}
#dd3{
background-color:blue;
left:400px;top:0;
}
#dd4{
background-color:black;
left:600px;top:0;
}
</style>
</head>
<body>
<div id="dd1"></div>
<div id="dd2"></div>
<div id="dd3"></div>
</body>
</html>
希望ID 为 #dd1 的 DIV 的宽和高能够在1000ms内从0渐变到200px,分20次进行渐变,于是没50ms渐变一次,高度或宽度都会变化10px。
通过老式的链式 setTimeout 我们可以这么做:
var oDD = document.getElementById('dd1');
var iTotalTime = 1000,//总时间
iTimes = 20,//渐变多少次
iPeriod = iTotalTime/iTimes;//渐变的周期
var i = 1;
function fChange()
{
if(i == (iTimes + 1)){
return;
}
oDD.style.width = i*10 + "px";
oDD.style.height = i*10 + "px";
i++;
setTimeout(arguments.callee,iPeriod);
}
setTimeout(fChange,iPeriod);
用我的模型可以这么做:
var oDD1 = document.getElementById('dd1');
var iTotalTime = 1000,//总时间
iTimes = 20,//渐变多少次
iPeriod = iTotalTime/iTimes;//渐变的周期
function fChange1()
{
if(typeof arguments.callee.i == 'undefined'){
arguments.callee.i = 0;
}
var i = arguments.callee.i++;
console.log('i= ' + i);
this.style.width = i*10 + "px";
this.style.height = i*10 + "px";
}
var oClock = new Clock(50),
oTimer = new Timer(oClock),//调度器
oCmdQueuer = new CmdQueuer(oTimer),//指令排队器
oCmd1 = new Cmd(oDD1,fChange1,[]);//指令,最终会有这样的调用fChange.apply(oDD)
//把 fChange 里面的自调度搬到 fChange 外面,由 oCmdQueuer 帮忙实现
for(var j = 0; j < iTimes; j++)
{
oCmdQueuer.addCmd(oCmd1,iPeriod);
}
oClock.start();
oCmdQueuer.start();
可以看到,两种代码的区别在于:
我的代码不再需要依赖链式调用,通过内部机制即可实现程序的串行执行。
四. 更多价值
1. 应付时间的变化
举个例子,参看上面的 html 代码,我们希望 #dd1 和 #dd2 串行地改变高度和宽度,代码如下。
var oDD1 = document.getElementById('dd1'); var oDD2 = document.getElementById('dd2'); function fChange1() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } var i = arguments.callee.i++; console.log('i= ' + i); this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } function fChange2() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } i = arguments.callee.i++; this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } function fChange3() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } i = arguments.callee.i++; this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } var oClock = new Clock(50), oTimer = new Timer(oClock),//调度器 oCmdQueuer = new CmdQueuer(oTimer),//指令排队器 oCmd1 = new Cmd(oDD1,fChange1,[]),//指令,最终会有这样的调用fChange.apply(oDD,[]) oCmd2 = new Cmd(oDD2,fChange2,[]);//指令,最终会有这样的调用fChange.apply(oDD,[]) //#dd1 开始样式变化 for(var j = 0; j < 20; j++) { oCmdQueuer.addCmd(oCmd1,50);//每 50ms 执行一次oCmd1,共20次 } //#dd2 在 #dd1 样式变化完成之后开始样式变化 for(var j = 0; j < 20; j++) { oCmdQueuer.addCmd(oCmd2,50);//每 50ms 执行一次oCmd2,共20次 } oClock.start(); oCmdQueuer.start();
如果后来需求发生了变化,要求 #dd1 在2s内完成转变,无论如何改动 #dd1 相关的代码,#dd2 相关的代码完全不需要变动。2. 规避链式调用
这是一个显而易见的好处,代码中你不再需要操心链式调用的问题。尤其是当你需要进行复杂的链式调用时。CmdQueuer 能帮您轻松地搞定这种串行关系。试想 A/B/C/D 要串行执行,间隔两秒,但是你无法预估A/B/C/D的执行各自需要多少秒,此时你不得不使用 setTimeout 链式调用,你将不得不在 A 的回调函数里调用 B , 在 B 的回调函数里调用 C,以此类推。一旦需求变化了,顺序打乱了,那就只能哭着改了。
五. 代码缺陷
Cmd 设计也许有缺陷。感觉怪怪的。仅能支持简单的串行执行;未对并发进行支持,因此很不强大。
六. 总结
也许是因为缺乏更具体更复杂的需求,实践的过程中总感觉想太多了。在这次实践中读者看不到的部分,我经历了许多封装变化、抽象、调试的过程。对观察者模式的应用已经深入骨髓了。差点就能用上模板模式,等时间宽裕了也许再来慢慢考虑吧。
七. 代码
可以拷贝以下代码运行看看效果:<html> <head> <style type="text/css"> body{ margin:0;padding:0;position:relative; } #dd1,#dd2,#dd3,#dd4{ position:absolute; } #dd1{ background-color:green; left:0;top:0; } #dd2{ background-color:yellow; left:200px;top:0; } #dd3{ background-color:blue; left:400px;top:0; } #dd4{ background-color:black; left:600px;top:0; } </style> </head> <body> <div id="dd1"></div> <div id="dd2"></div> <div id="dd3"></div> <script type="text/javascript"> function Clock(iInClockCycle) { var iClockCycle = iInClockCycle; this.sStatus = "stop"; this.iClockTick = 0; this.aObserver = []; this.getClockCycle = function(){ return iClockCycle;//私有,注意,只提供了get方法 }; } Clock.prototype.getStatus = function(){ return this.sStatus; }; Clock.prototype.getClockTick = function(){ return this.iClockTick; }; Clock.prototype.incClockTick = function(){ this.iClockTick++; this.notifyObservers(); }; Clock.prototype.addObserver = function(oInObserver) { this.aObserver.push(oInObserver); }; Clock.prototype.delObserver = function(oInObserver) { var aNew = [], k = -1; for(k in this.aObserver) { if(this.aObserver[k] !== oInObserver) { aNew.push(this.aObserver[k]); } } this.aObserver = aNew.slice(0);//避免直接等于aNew,内存问题 }; Clock.prototype.notifyObservers = function(oInObserver) { var k = -1; for(k in this.aObserver){ this.aObserver[k].clockArise();//时钟上升沿 } }; Clock.prototype.start = function() { this.sStatus = "run"; var _this = this, iCC = this.getClockCycle(); function clockRise() { _this.incClockTick(); console.log(_this.getClockTick()); if(_this.getStatus() == "stop"){ return; } setTimeout(arguments.callee,iCC); } setTimeout(clockRise,iCC); }; Clock.prototype.stop = function(){ this.sStatus = "stop"; }; //能够简化渐变应用的底层类 //包括 调度器 命令排队器 命令 三个类 //基本构造器构造函数 function Timer(oClock) { this.oClock = oClock;//以时钟为准 //对象数组存储被传送入本调度器的命令以及相关信息 //{命令,时间除以时钟周期,注册到调度器的时钟滴答} this.aCmdInfo = []; this.oClock.addObserver(this); } //把 this.aCmdInfo 看成一个有序队列, 排序规则是先执行的命令在前 Timer.prototype.addCmd = function(oCmd,iInterval) { if(! this.checkPara(oCmd,iInterval)){ return false; } //拼装 var oInfo = { 'cmd':oCmd, 'reg':this.oClock.getClockTick(), 'intv':Math.ceil(iInterval/this.oClock.getClockCycle()) }; this.aCmdInfo.push(oInfo); this.aCmdInfo.sort(function(oA,oB){ var iTimeA = oA['reg'] + oA['ocp'], iTimeB = oB['reg']+oB['ocp']; if(iTimeA < iTimeB){ return -1; } else if(iTimeA > iTimeB){ return 1; } else if(oA['reg'] <= oB['reg']){ return -1; } return 0; }); return true; }; //调度器作为观察者,侦听时钟上升沿 Timer.prototype.clockArise = function() { //取队首 if(this.aCmdInfo.length > 0) { var oCur = this.aCmdInfo.shift(); if(oCur['reg']+oCur['intv'] <= this.oClock.getClockTick()) { oCur['cmd'].timeout(); } else { this.aCmdInfo.unshift(oCur); } } }; //检查参数 //参数是命令(oInCmd) //调用的周期(iInPeriod) //执行的次数(excution times) Timer.prototype.checkPara = function(oInCmd,iInInterval) { var iClockCycle = this.oClock.getClockCycle();//获取时钟周期 //霍,这么牛逼的检查 //instanceof Cmd 针对接口编程,看看oInCmd 是不是一个Cmd //至于 oInCmd 是哪种 Cmd ,不关心 if(oInCmd instanceof Cmd && typeof iInInterval == 'number' && iInInterval >= 0) { //商,判断商是不是一个整数;判断执行次数是不是整数 var iQuotient = iInInterval/iClockCycle; if(Math.floor(iQuotient) == Math.ceil(iQuotient)) { return true; } } return false; }; //Queuer 需要具备以下能力 //了解一组命令是否执行完成 //当一组命令执行完之后 //向调度器输送另一组命令 //排队器 和 调度器 之间的关系适用 组合, HAS-A 关系; //排队器 和 命令 之间的关系适用观察者模式 function CmdQueuer(oInTimer) { this.oTimer = oInTimer; this.aCmdInfo = []; this._this = this; } //执行队列最前端的命令 CmdQueuer.prototype.solveNext = function() { if(this.aCmdInfo.length > 0){ var oCmdInfo = this.aCmdInfo.shift(); this.oTimer.addCmd(oCmdInfo['cmd'],oCmdInfo['intv']); } }; CmdQueuer.prototype.start = function() { this.solveNext(); }; CmdQueuer.prototype.checkPara = function(oInCmd,iInInterval) { //检查参数,借用了调度器的检查方法 //不自行编写代码做检查,以便将这种变化留在调度器 if(! this.oTimer.checkPara(oInCmd,iInInterval)){ return false; } return true; }; CmdQueuer.prototype.addCmd = function(oInCmd,iInInterval) { if(! this.checkPara(oInCmd,iInInterval)){ return false; } //排队器侦听命令的执行结束 oInCmd.addFinishObs(this._this); //把命令加到排队器的队列中 this.aCmdInfo.push({'cmd':oInCmd,'intv':iInInterval}); }; //命令完成时调用此函数告知排队器 //排队器在这个函数中删除该命令 //注意,命令可能在排队器中注册多次 //只删一个命令 CmdQueuer.prototype.cmdFinish = function(oCmd) { console.log('finfinfifnfifnfifn'); this.solveNext(); }; function Cmd(oInContext,oInFunc,aInArg) { this.aStartObs = []; this.aFinishObs = []; this.oContext = oInContext; this.oFunc = oInFunc; this.aArg = aInArg; this._this = this; } //计时器超时时调用 Cmd.prototype.timeout = function() { this.notifyStartObs(); this.execute(); this.notifyFinishObs(); }; Cmd.prototype.execute = function() { this.oFunc.apply(this.oContext,this.aArg); } //一个命令可以被注册到 排队器 多次 //排队器观察命令执行结束的状态 Cmd.prototype.addFinishObs = function(oInObs){ this.aFinishObs.push(oInObs); }; Cmd.prototype.addStartObs = function(oInObs){ this.aStartObs.push(oInObs); }; //命令不支持注册到多个排队器但 //可能注册到同一个排队器多次 //所以可以保证每次删除的都是同一个排队器 //这里我把问题简化了,不然很纠结 Cmd.prototype.notifyFinishObs = function() { //删除一个排队器并通知它 var oObs = null; if(oObs = this.aFinishObs.shift()){ oObs.cmdFinish(this._this); } }; Cmd.prototype.notifyStartObs = function() { //删除一个排队器并通知它 var oObs = null; if(oObs = this.aStartObs.shift()){ oObs.cmdStart(this._this); } }; var oDD1 = document.getElementById('dd1'); var oDD2 = document.getElementById('dd2'); var oDD3 = document.getElementById('dd3'); var iTotalTime = 1000,//总时间 iTimes = 20,//渐变多少次 iPeriod = iTotalTime/iTimes;//渐变的周期 function fChange1() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } var i = arguments.callee.i++; console.log('i= ' + i); this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } function fChange2() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } i = arguments.callee.i++; this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } function fChange3() { if(typeof arguments.callee.i == 'undefined'){ arguments.callee.i = 0; } i = arguments.callee.i++; this.style.width = i*10 + "px"; this.style.height = i*10 + "px"; } var oClock = new Clock(50), oTimer = new Timer(oClock),//调度器 oCmdQueuer = new CmdQueuer(oTimer),//指令排队器 oCmdQueuer1 = new CmdQueuer(oTimer),//指令排队器 oCmd1 = new Cmd(oDD1,fChange1,[]),//指令,最终会有这样的调用fChange.apply(oDD) oCmd2 = new Cmd(oDD2,fChange2,[]),//指令,最终会有这样的调用fChange.apply(oDD) oCmd3 = new Cmd(oDD3,fChange3,[]);//指令,最终会有这样的调用fChange.apply(oDD) //把 fChange 里面的自调度搬到 fChange 外面,由 oCmdQueuer 帮忙实现 //当向 addCmd 提供参数 true 时 //oCmdQueuer 认为,当前这个命令必须在前一个命令执行完毕之后才开始执行 for(var j = 0; j < iTimes; j++) { oCmdQueuer.addCmd(oCmd1,iPeriod); } for(var j = 0; j < iTimes; j++) { oCmdQueuer.addCmd(oCmd2,iPeriod); } for(var j = 0; j < iTimes; j++) { oCmdQueuer1.addCmd(oCmd3,iPeriod); } oClock.start(); oCmdQueuer.start(); oCmdQueuer1.start(); </script> </body> </html>
附录,编程中随意记录的,无益,勿读
阶段一
先把阶段性或者转折点的程序记录下来
//CSSEle function CSSEle(oInEle) { var oEle = oInEle; //为了避免使用 this.oEle 从而把 this.oEle 暴露 //采用私用函数访问私有变量 //原型函数调用私有变量 function getEle() { //可以通过作用域访问到私有变量oEle return oEle; } function setEle(oInEle) { oEle = oInEle; } } //通过原型提供获取和设置 DOM 对象的接口 CSSEle.prototye.getEle = function() { return getEle(); }; CSSEle.prototye.setEle = function(oInEle) { return setEle(oInEle); }; //取得数字类型的style,比方zIndex //为了避免重复设计,因而提取出共性需求,重复设计的例子是: //CSSCmdAddHeight 需要使用 getHeight 获取元素高度 //CSSCmdAddWidth 需要使用 getWidth 获取元素宽度 //CSSCmdAddZIndex 需要使用 getZIndex 获取zIndex值 //而处理方式是很类似的 CSSEle.prototye.getNumericStyle = function(sInStyleName) { //先要判断是否属性值是否存在,如果不存在返回null //如实汇报,而不是擅作主张返回某种默认值 if(typeof this.style[sInStyleName] =='string' && /^\d+/.test(this.style[sInStyleName])) { return parseInt(this.style[sInStyleName]); } else { return null; } }; //设置某个属性值的接口需求是非常一致的,就是设置某属性值为某字符串 CSSEle.prototye.setStyle = function(sInStyleName,sInStyleValue) { this.style[sInStyleName] = sInStyleValue; return true; }; //既可以作为接口或者抽象类,虽然这个接口里只有一个函数,似乎完全没有必要实现此接口 //但是有两点理由说明这个接口是值得的 //一 是体现这种概念上的继承 //二 是也许有些共性需求暂时还没有被发现 function CSSCmd() { this.execute = function(){}; } function CSSCmdAddHeight(oCSSEle,iInPlus) { //如果不信任传入的参数,可以做必要的类型和范围检查 //后续考虑 var oCSSEle = oInCSSEle; var iPlus = iInPlus; //以这种方式体现 JAVA 中的"实现接口(implement interface)"是很贴切的 //但是,如果作为扩展某抽象类,那就不行了 //原因不在于抽象类中有抽象方法,而是在于抽象类中可能有非抽象的方法 //这些非抽象方法应该是共享的,以这种方式实现就不好了,无法共享 //嗯,还是觉得通过原型链继承的好 CSSCmd.apply(this); this.execute = function() { //这里封装了算法逻辑 //如果没有获取到有效的高度,会返回null var iHeight = oCSSEle.getNumericStyle('height'); //注意! 表达式 typeof null == "object" 成立 if(typeof iHeight == "number") { return oCSSEle.setStyle('height',(iHeight+iPlus)+"px"); } return false; }; } function CSSCmdAddWidth(oInCSSEle,iInPlus) { // var oCSSEle = oInCSSEle; var iPlus = iInPlus; CSSCmd.apply(this); this.execute = function() { var iWidth = oCSSEle.getNumericStyle('width'); if(typeof iWidth == 'number') { return oCSSEle.setStyle('width',(iWidth+iPlus)+"px"); } return false; }; };
此时遇到的问题是,CSSCmdAddWidth 和 CSSCmdAddHeight 在属性上和 execute 函数上存在极大的相似性,是否有必要对它们做进一步抽象,于是形成在 CSSCmd 和 CSSCmdAddWidth 之间的抽象层?另一个问题是,CSS属性那么多,虽然目前我只看到了 height width top zIndex 这些数值型的属性,但后期也许会有非数值型的属性加进来,莫非每个属性都设置一个类?假设所有属性攻N种,那么最坏情况是,需要实现 N 个类?
还有,如何应对一个命令里设置多个属性值的问题?这种组合问题难道要造成类的爆炸? N*N*N
至此我开始觉得,ADDWidth ADDHeight 等只不过是设置属性值的过程中可能用到的一个算法。且此算法有一个骨架,就是 先提取 ,后计算 ,再赋值。而提到提取算法骨架,并提供给子类良好的扩展能力,模版方法模式最合适了。
问题的本质到底是什么?
1. 给我一个DOM对象,指出它的一个CSS属性,给我一个起始属性值,给我一个属性值关于时间的函数,给我总时间,我能按照此函数设置对象的属性值
2. 给我一个DOM对象,指出多个CSS属性,我能实现多个属性按照自己的方式同时变化
3. 给我多个DOM对象,任意多属性,我能实现它们的属性值按照自己的意愿同时变化
//CSSChanger是个无比牛逼的东西 //可以添加多个元素,每个元素可以多个属性,每个属性有可以有自己的变化方案 //基于队列,可以暂停队列,可以清除队列 function CSSChanger() { //status 指出当前任务队列中 var status = "editable";//还有 running paused finished //判断是否针对编辑锁定,编辑是指添加删除任务 function getStatus() { return status; } function setStatus(sInStatus) { status = sInStatus; } } this.prototype.isEditLocked = function() { if(getStatus() == 'editable'){ return true; } return false; }; this.prototype.startChanger = function(){}; //暂停变化,意思是可以恢复,也许这对资源消耗的控制是有帮助的 this.prototype.pauseChanger = function(){}; //删除变化,意思是删除后续的操作 //但之前的操作无法挽回,之前操作的记录也消失 this.prototype.clearChanger = function(){}; //恢复Changer执行之前的状态 //即撤销任何已经执行的变化,但依然保持变化的能力 this.prototype.recoverChanger = function(){}; //针对某个对象的某个属性应用变化 this.prototype.addItem = function(oInEle,sInStyleName, sInStyleValueInit,iInTimeout,iInInterval,fHandler){}; //可以支持针对同一个对象的多个属性同时应用各自的变化 this.prototype.addItems = function(oInEle,aInStyleNames, aInStyleValuesInit,iInTimeout,iInInterval,aInHandlers){};
写到这里,隐隐约约感觉这种设计背后必然有大的问题。思考良久,没错,违反了单一职责原则。一旦这么设计,在执行变化的过程中,一方面 changer 需要在时间上进行合理的调度,另一方面,它不得不亲自动手去更改DOM对象的的样式。这种耦合使得一个类不得不应对两种变化,一个是调度算法的变化,一个是设置DOM对象样式使用的方式(一定是 oElement.style.propertyName = propertyValue 这种形式吗?!)的变化。
于是,应该产生 调度器(Scheduler) 和 CSSCmd 两个概念。调度器只对命令的调度负责,而 CSSCmd 则是被调度器调度的命令,它必须向调度器提供一个接口,使得调度器发现轮到这个Cmd执行时,可以即时地通知它。(观察者模式实现解耦,解什么藕?使用统一的接口告知观察者,此接口是不会变的。否则,假如CmdA有权使得调度器执行函数 fA, CmdB有权使得调度器执行 fB,而后来 fA 被删除了,那么这种变化将被扩散到调度器。这就是耦合)
在这里,有一个疑问,真的是观察者模式?
观察者模式定义如下: 观察者模式定义了对象之间的一对多依赖( 这一点 是满足的),这样一来,当一个对象改变状态时(通知CMD之前,调度器的状态的确改变了?没错,看看下文中的实现就知道),它的所有依赖者都会收到通知并自动更新(这一点我做到了)。
不过,在我接下来的实现中,观察者并不需要到被观察者那里拉取数据。但无所谓撒,依然是观察者模式。
为什么是通知 Cmd ,而不是直接执行 Cmd ?如果能够保证 Cmd 接到通知之后一定会执行,那么没错,效果是一样的。不过为了维持一点弹性,还是选择通知方式吧。
那么 Cmd 收到调度器的通知之后,它极可能开始运行,于是存在两个问题:
1. 如何处理 Cmd 长时间运行的情况
2. Cmd 里又自行设置了 setTimeout 或 setInterval 怎么办
2. Cmd 运行完毕如何通知调度器
处理办法
1. 设置超时时长,通知 cmd 之后开始计时等待 cmd 的消息。一旦CMD出于某种原因没有通知调度器,那么超时。调度器不再等待,继续执行。
2. 不建议 cmd 中使用 setTimeout 等,这就靠程序员自己努力了
先不管 CSSCmd 如何实现,既然已经把调度器的概念独立出来了,那么现在再来实现也许会轻松地多。
来吧,用顽强的注释解释稀稀拉拉的代码。
//整的框架就是
function Schedular(iInMinInterval) { //调度器的原理是每隔一段时间扫描一下 //这个私有变量只在初始化时设置,不允许中途改变 var iMinInterval = iInMinInterval; //为了不让私有变量被中途改变,不得不如此浪费 //如果允许修改私有变量,那么大可不必如此波折 //直接作为对象的属性就好了,因为访问已不可控制 this.getMinInterval = function(){ return iMinInterval; } //对象数组存储被传送入本调度器的命令以及相关信息 //每个对象是一个五元组 //命令 周期除以iMinInterval 共执行多少次 已执行多少次 //注册到调度器时的 iClock 值 this.aCmdInfo = []; this.sStatus = "pause";//还可以是"run" this.iClock = 0;//当前已经经过了多少个最小时间间隔(iMinInterval) this.iMaxClock = Math.pow(2,64)-1;//绝对够用了 } Schedular.prototype.fMulticast = function(){} Schedular.prototype.fMinInterHandler = function(){}; //检查参数 Schedular.prototype.checkPara = function(oInCmd,iInPeriod,iInExcutionTimes){}; //参数 //命令(oInCmd) //调用的周期(iInPeriod) //执行的次数(excution times) //注册一个指令oInCmd,这个指令在iInExcutionTimes 的时间内,以 iInPeriod 为周期,执行多次 Schedular.prototype.addCmd = function(oInCmd,iInPeriod,iInExcutionTimes){}; Schedular.prototype.start = function(){}; Schedular.prototype.pause = function(){}; Schedular.prototype.delete = function(){};
2 . 调度器实现了
经过重重对变化的发掘和分离,终于发现,其实我最开始想要的是一个简单的调度器。通过想调度器注册一个命令并提供一个时间值,那么,调度器就会在那个时间之后通知命令去执行。
最开始,没有抓住这个本质,于是把调度多少次、甚至调度的时候样式发生了什么变化都杂糅到一起,难免会心里犯嘀咕。至此,越发觉得设计模式真的可以让编程变得简单,让程序具有思想,让程序员喜爱自己的程序。
我实现的调度器的基本原理就是,它具有自己的时钟周期,每个周期的结束,它都会查阅所有注册到调度器上的命令,如果这个命令该执行了,那就通知这个命令,顺便从调度器中删除这个命令。没错,它只做一件事情:调度!所谓调度就是指时间到了就通知被调度的人。这种由一个对象的状态出发N个对象的行为的功能,没错,适合用观察者模式实现。
在逐渐剥离出调度器模型的过程中,对我的编程影响比较大的是,单一职责原则,针对接口编程,封装变化。其中,前两者在我的代码或注释中能够体现,至于第三者,主要体现在从最开始到最终实现调度器模型的整个过程中。
关于编程技巧(在我看来,编程技巧和语言的耦合较大),在实现模型的过程中,主要就碰到了一个问题。在setTimeout 的函数参数中使用 this 指针,特殊之处在于在这个参数函数中的this指针指向window。作为刚刚入门的小菜鸟,我发现自己对 this 的理解还不够深入,因而容易糊涂。当然,这个问题最终还是被我解决了,在这个函数参数之内通过作用域链引用函数之外的变量。在尝试的过程中,我还考虑到把 实际应该用到的this存入变量 _this , 然后作为函数参数的属性,种种,后续还需总结。
接下来就是我的代码啦,虽然在大牛眼里我这代码实在不能再简单,不过我还是多写一点注释,希望能讲清楚背后的设计理念、编程技巧以及体现编码风格。
//基本构造器构造函数 function Schedular(iInClockCycle) { //调度器的原理是每隔一段时间扫描一下 //这个私有变量只在初始化时设置,不允许中途改变 var iClockCycle = iInClockCycle; //当前时钟是暂停了还是运行中,值还可以是"run" this.sStatus = "pause"; //对象数组存储被传送入本调度器的命令以及相关信息 //{命令,时间除以时钟周期,注册到调度器的时钟滴答} this.aCmdInfo = []; //当前的时钟滴答数 this.iClockTick = 0; //时钟滴答数的最大值 this.iMaxClockTick = Math.pow(2,64)-1;//绝对够用了 //setTimeout参数函数中的this指针问题,哎 var _this = this; this.getThis = function(){ return _this; } //为了不让私有变量被中途改变,不得不如此浪费 //如果允许修改私有变量,那么大可不必如此波折 //直接作为对象的属性就好了,因为访问已不可控制 this.getClockCycle = function(){ return iClockCycle; }; } Schedular.prototype.fMulticast = function() { //我们假设数组有序,需要先执行的命令在数组首部,命令执行后删除 //for(var iIdx=0,iLen=this.aCmdInfo.length; i < iLen;i++) while(this.aCmdInfo.length > 0) { var oCmdInfo = this.aCmdInfo.shift();//弹出数组首部 if(oCmdInfo['reg']+oCmdInfo['ocp'] == this.iClockTick){ oCmdInfo['cmd'].notify(); } else { this.aCmdInfo.unshift(oCmdInfo);//压回去 break; } } }; //检查参数 //参数是命令(oInCmd) //调用的周期(iInPeriod) //执行的次数(excution times) Schedular.prototype.checkPara = function(oInCmd,iInInterval) { var iClockCycle = this.getClockCycle();//获取时钟周期 //霍,这么牛逼的检查 //instanceof Cmd 针对接口编程,看看oInCmd 是不是一个Cmd //至于 oInCmd 是哪种 Cmd ,不关心 if(oInCmd instanceof Cmd && typeof iInInterval == 'number' && iInInterval >= 0) { //商,判断商是不是一个整数;判断执行次数是不是整数 var iQuotient = iInInterval/iClockCycle; if(Math.floor(iQuotient) == Math.ceil(iQuotient)) { return true; } } return false; }; //参数是命令(oInCmd) //时隔多久调用命令(iInInterval) Schedular.prototype.addCmd = function(oInCmd,iInInterval) { if(! this.checkPara(oInCmd,iInInterval)){ return false; } this.aCmdInfo.push( { 'cmd':oInCmd, 'ocp':iInInterval/this.getClockCycle(),//占据多少个时钟周期 'reg':this.iClockTick } ); //console.log("增加了命令,此时滴答为:",this.iClockTick ); //alert("增加了命令,此时滴答为:" + this.iClockTick); //对数组排序使得需要先执行的命令放在数组首部 this.aCmdInfo.sort(function(oA,oB){ var iTimeA = oA['reg'] + oA['ocp'], iTimeB = oB['reg']+oB['ocp']; if(iTimeA < iTimeB){ return -1 } else if(iTimeA > iTimeB){ return 1; } else if(oA['reg'] <= oB['reg']){ return -1; } return 0; }); return true; }; Schedular.prototype.start = function() { this.sStatus = "run"; //为什么不敢这么做:_this = this;//报错,意思是我依赖执行时的变量 var _this = this.getThis(); //我也想这么写Schedular.prototype.fClockHandler,于是更不对了 //指定 Schedular.prototype.fClockHandler 函数的某属性为某 Schedular对象 //也不行,依赖执行时的变量 //所以这种做法还是相当无奈,但是有技巧的,看来得抽空总结下this指针啊 function fClockHandler() { //多播,只通知某一部分符合要求的命令 _this.fMulticast(); _this.iClockTick = (_this.iClockTick+1)%_this.iMaxClockTick; //console.log("在fClockHandler中,时钟滴答是 ============",_this.iClockTick); //console.log("在fClockHandler中,this.sStatus========== ",_this.sStatus); if(_this.sStatus == "pause"){ return false;//已经暂停就啥也不干了 } //console.log("在fClockHandler中,时钟滴答是 ===========",_this.iClockTick); setTimeout(arguments.callee,_this.getClockCycle()); return true; } setTimeout(fClockHandler,this.getClockCycle()); }; Schedular.prototype.pause = function() { this.sStatus = "pause"; }; Schedular.prototype.delete = function() { //担心观察者依赖调度器的数据?! //放下成见吧,调度器本身没什么好让观察者依赖的 //调度器的定义本身不是信息的提供者 //尽管如此,它依然可以作为被观察者,牛逼了!~ this.aCmdInfo = []; this.sStatus = "pause"; this.iClockTick = 0; }; //测试一下 function Cmd() { this.cnt = 0; this.notify = function(oInSubject){ this.cnt++; console.log("我是命令" , this.cnt); alert("我是命令" + this.cnt); }; } //调度器的时钟周期是1000ms var oSchedular = new Schedular(1000);//误差是1000ms var oCmd = new Cmd(); oSchedular.start();//从此刻开始计时 oSchedular.addCmd(oCmd,4000); //oSchedular.pause(); setTimeout(function(){ oSchedular.addCmd(oCmd,4000); },4100); //为啥不是4100,有道理的,调调看吧
3. 调度器制造烦恼
是的,调度器模型已经建立起来并且可以投入使用了。但是,它展现出的能力似乎不是那么强。例如,假设我有两个调度任务A和B,B必须等待A执行完才能执行,如此一来,我辛苦构建的调度器就爱莫能助了。强忍着没有女朋友的痛苦,我躺在床上想了好久,除非符合设计模式理念,否则我将没有信心把支持这种任务依赖的代码添加到我的调度器模型里面去。
想了半天,我忽然想通了,答案是:不能把这种依赖的逻辑整合到调度器类里去。
我要说明的两点内容是
1. 我当前实现的这个调度器的功能不是很纯粹。之前我说它是单一职责的,现在看来只能算是基本正确,因为,这个调度器一方面整合了一个时钟,另一方面利用时钟提供了计时器功能,因此从功能上看,它具备这样的能力:在给定的时间执行给定的任务。或者更优雅地讲:在给定的时间触发给定的事件。
2. 至于oCmdB依赖于oCmdA的完成,本质上是使用调度器,而不是调度器的一部分。假设oCmdA执行过程的时间是 iTime , 那么实际上相当于调用
oSchedular.addCmd(oCmdA,timeAStart);
oSchedular.addCmd(oCmdB,timeAStart + iTime);// 于是等到了oCmdA执行完oCmdB才开始执行
但问题是,iTime 是多少是无法评估的,所以冥冥中才会有应该由 调度器亲自控制的念头。总之,苦恼的原因在于,发现了一种与调度器逻辑无关的需求:如果确定一个命令执行的总时间,以便安排另一个命令执行的执行时间。
得出了这个结论,我很庆幸,因为修改之前的代码总不是很愉悦的过程。
4. 满足新需求--实现排队器
实际上更普遍的需求是:一组命令必须在一组命令之后执行,其中,每组命令至少包含一个命令。
这个问题不复杂,更不需要使用拓扑排序。对问题的形象化描述以及解决方案是:
有A/B/C三组命令,执行的顺序必须是A-->B-->C,作为万能的上帝,我先把A喂给调度器,A执行完之后,我从调度器哪里获得这个消息,然后我再把B喂给调度器,B完了喂C。
先把上帝的角色抽象一下吧,起名为 Queuer ,意思是排队器。
再具体一点描述如何实现,假设三个组各包含一些命令,a1 a2 | b1 b2 | c1 c2 c3 c4 。只要分三组传入只要 b1 和 c1 各自指出自己不愿和之前的命令并行,其余命令指出自己可以接受并行即可。例如,可以这样处理
var oQueuer = new Queuer(各种参数);
oQueuer.addCmd(a1,100,true);//要求与 a1 之前的命令隔开 ,100ms 之后执行,要求等待a1之前的所有命令执行完之后再开始计时,计时100ms后执行
oQueuer.addCmd(a2,100,false); //不要求与 a1 隔开,于是和a1并发执行了
oQueuer.addCmd(b1,200,true);// 于是,等到 a1 a2 都执行完了,b1才开始计时
oQueuer.addCmd(b1,300,true);//于是,等 b1 开始后 100ms , b2 才开始执行
现在缕一缕调度器、排队器、命令之间的关系。排队器接受命令的注册,排队器被告知(但不是被命令告知,这不是命令的职责)命令是否希望等待之前的命令执行完才执行。假如一个命令被要求在之前的命令执行完之后才执行,那么排队器就有必要知道命令什么时候结束。
调度器依赖于排队器,使用组合;
调度器安排命令的执行
调度器等待命令执行完,适用观察者模式
调度器接收来自排队器的命令,并在指定的时间运行
贴代码比讲解容易,来吧~
//Queuer 需要具备以下能力 //了解一组命令是否执行完成 //当一组命令执行完之后 //向调度器输送另一组命令 //排队器 和 调度器 之间的关系适用 组合, HAS-A 关系; //排队器 和 命令 之间的关系适用观察者模式 function CmdQueuer(oInSchedular) { this.oSchedular = oInSchedular; this.aCmdInfo = []; this.aCmdWaited = []; this._this = this; } //执行队列最前端的命令 CmdQueuer.prototype.executeCmd = function(oCmdInfo) { this.aCmdWaited.push(oCmdInfo['cmd']); this.oSchedular.addCmd(oCmdInfo['cmd'],oCmdInfo['intv']); }; //这个函数是命令排队器中比较复杂的逻辑了,但是还是比较简单 CmdQueuer.prototype.executeGroup = function() { if(this.aCmdInfo.length == 0){ return false; } if(this.aCmdInfo[0]['wait'] == true && this.aCmdWaited.length > 0){ return false; } if(this.aCmdInfo[0]['wait'] == true && this.aCmdWaited.length == 0){ //执行this.aCmdInfo[0] this.executeCmd(this.aCmdInfo.shift()); } //执行队首所有 wait 值为 false 的 var oCmdInfo = {}; while(oCmdInfo = this.aCmdInfo.shift()) { if(oCmdInfo['wait'] == true){ this.aCmdInfo.unshift(oCmdInfo); break; } this.executeCmd(oCmdInfo); } return true; }; CmdQueuer.prototype.checkPara = function(oInCmd,iInInterval,bInWait) { //检查参数,借用了调度器的检查方法 //不自行编写代码做检查,以便将这种变化留在调度器 if(! this.oSchedular.checkPara(oInCmd,iInInterval)){ return false; } return true; }; CmdQueuer.prototype.addCmd = function(oInCmd,iInInterval,bInWait) { if(! this.checkPara(oInCmd,iInInterval,bInWait)){ return false; } //排队器侦听命令的执行结束 oInCmd.addFinObs(this._this); //把命令加到排队器的队列中 this.aCmdInfo.push({'cmd':oInCmd,'intv':iInInterval,'wait':bInWait}); //执行一组意思是执行队列首部可以同步执行的命令 this.executeGroup(); }; //命令完成时调用此函数告知排队器 //排队器在这个函数中删除该命令 //注意,命令可能在排队器中注册多次 //只删一个命令 CmdQueuer.prototype.cmdFin = function(oCmd) { for(var i=0, iLen=this.aCmdWaited.length;i<iLen;i++) { if(this.aCmdWaited[i] === oCmd){ this.aCmdWaited.splice(i,1);//删除第i个你懂的 break; } } this.executeGroup(); };
这里涉及了简单的 Cmd 类,以供测试:function Cmd(oInContext,oInFunc) { this.aFinObs = []; this.oContext = oInContext; this.oFunc = oInFunc; this._this = this; } Cmd.prototype.notify = function() { this.execute(); this.notifyFinObs(); }; Cmd.prototype.execute = function() { alert('aa'); } //一个命令可以被注册到 排队器 多次 //排队器观察命令执行结束的状态 Cmd.prototype.addFinObs = function(oInObs){ this.aFinObs.push(oInObs); }; //命令不支持注册到多个排队器但 //可能注册到同一个排队器多次 //所以可以保证每次删除的都是同一个排队器 //这里我把问题简化了,不然很纠结 Cmd.prototype.notifyFinObs = function() { //删除一个排队器并通知它 var oObs = this.aFinObs.shift(); oObs.cmdFin(this._this); };
然后写几行测试代码//调度器的时钟周期是50ms,周期多大,误差就多大,没办法 var oSchedular = new Schedular(50); var oCmdQueuer = new CmdQueuer(oSchedular); var oCmd = new Cmd(); oSchedular.start();//务必开启调度器 oCmdQueuer.addCmd(oCmd,2000,false);//第二秒执行 oCmdQueuer.addCmd(oCmd,2000,true);//第四秒才执行 oCmdQueuer.addCmd(oCmd,0,false);//第四秒之后的第50ms执行 //oCmdQueuer.addCmd(oCmd,50,false); //oSchedular.pause();//会导致命令都暂停,看不到输出
5. 来吧,想想 Cmd 到底是什么概念