Javascript实现任务队列(异步)山寨版
问题起因
在图片导航文章中,用户点击“上一张”或“下一张”需要等待图片完全移到位后,按钮才可以继续使用,在图片移动过程中,用户不能点击按钮,或点击后无任何效果,这就容易让用户产生挫败感,但如果让用户每次点击均即时响应,又会让坐标或状态出现混乱。
解决思路
考虑到可以使用一个任务队列,用户的每次点击,均予以响应(解决挫败感),并将作为一个任务,由任务队列接收。
function movePanel() { var tq=taskQueue(); this.last = function (){ if (!this.getCanLast()) return; tq.addTask(function(){...}); }; this.next = function (){ if (!this.getCanNext()) return; tq.addTask(function(){...}); }; }
而任务队列的任务是按进入队列的顺序延迟执行(解决状态一致性)的,即当前一个任务完成后,后面的任务才被执行,如果当前没有任务,则入队列的任务立即执行,这样就可以保证movePanel内部的状态的一致性。
function taskQueue() { var taskList = []; var isRun = false; this.addTask = function (task) { taskList.push(task); }; setInterval(function () { if (taskList.length > 0 && !isRun) { isRun = true; taskList.shift()(); isRun = false; } }, 100); }
这里面一个task就一个函数,很明显,这个方案太简单,如果task是一个异步的函数,调用它时会立即返回并执行下一条isRun=false语句,并进行下一个task的执行,而此时当前的task并没有执行完毕,从而坐标或状态的不一致性又会出现。接下来,我们为task为异步的情况,再进行设计:
function taskQueue() { var taskList = []; var isRun = false; this.addTask = function (task) { taskList.push(task); } function run(task) { var endWrap = function () { if(task.end) task.end(); isRun = false; }; if (task.isAsyn) task.run(endWrap); else { task.run(); endWrap(); } }; setInterval(function () { if (taskList.length > 0 && !isRun) { isRun = true; run(taskList.shift()); } }, 100); }
我们与调用方做个约定,将task作为一个对象,它拥有isAsyn的属性表示是否是一个异步任务,具有run和end的方法,如果是异步任务,必须提供一个end方法,并作为run的一个参数,run执行完毕后,必须回调这个end方法。
function movePanel() { var tq=taskQueue(); this.last = function (){ if (!this.getCanLast()) return; tq.addTask({isAsyn:true, run: function (end) {... end(x,y);}, end: function (x,y) {} }); }; this.next = function (){ if (!this.getCanNext()) return; tq.addTask({isAsyn:true, run: function (end) {... end(x,y);}, end: function (x,y) {} }); }; }
理论上没有问题,但实际上,回调函数end也是需要参数的,比如我们的movePanel中end,是有一个x和y的坐标传给这个回调的,而我们的taskQueue将end方法拦截后进行包装后再调用,即使run方法传给end的参数也在这个过程中被无情的忽略了,运行效果并不是我们期望的。还好,我们有apply,可以将这些参数继续传递下去:
function taskQueue() { var taskList = []; var isRun = false; this.addTask = function (task) { taskList.push(task); } function run(task) { var endWrap = function () { if(task.end) task.end.apply(task,arguments); isRun = false; }; if (task.isAsyn) task.run(endWrap); else { task.run(); endWrap(); } }; setInterval(function () { if (taskList.length > 0 && !isRun) { isRun = true; run(taskList.shift()); } }, 100); }
这样参数是可以传递了,但新的问题出现了,用户点击次数过狠后,图片移出了相应的范围,也就是说原先在接受任务时的判断if (!this.getCanNext()) 或if (!this.getCanLast()) 是通过了,但到真正这个任务被执行时,其实已不满足这个判断条件了。所以可以考虑给task再增加一个validate方法,用于检测此时是否有必要再执行先前已入队列的任务。以下是考虑加了validate及task当前状态的后完整山寨版:
function taskQueue() { var taskList = []; var isRun = false; this.addTask = function (task) { task.status = 'waiting'; taskList.push(task); }; function run(task) { if (!task.validate()) { task.status = 'invalidate'; isRun = false; return; } function endWrap() { if(task.end) task.end.apply(task,arguments); task.status = 'done'; isRun = false; }; task.status = 'running'; if (task.isAsyn) { if(task.begin) task.begin(); task.run(endWrap); } else { if(task.begin) task.begin(); task.result = task.run(); endWrap(); } } setInterval(function () { if (taskList.length > 0 && !isRun) { isRun = true; run(taskList.shift()); } }, 100); }
我们给task增加了begin的方法,这是供该task被执行时可以被回调,增加了result属性,如果是同步的task,被执行时可以得到返回值。至于异常,只能task自己去保证不会抛出异常,否则这个队列会一直阻塞。
我们再修改movePanel中的last和next方法,以期达到我们希望的效果。
至此,我们的taskQueue已帮助我们达到需要的效果,如果期望它更完善,就需要另外的应用加以验证,通过再次重构达到复用。