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已帮助我们达到需要的效果,如果期望它更完善,就需要另外的应用加以验证,通过再次重构达到复用。

posted @ 2011-04-01 15:42  他山之石_  阅读(3006)  评论(0编辑  收藏  举报