用NodeJs实现延迟调用,规避定时任务的闭包问题

很多人在用NodeJs的setTimeout(callback, delay[, arg][, ...])编写定时任务时,习惯上直接操作callback外部的对象object(闭包的特点)。这样做有一个隐患,就是当callback真正执行的时候,外部对象object可能已经被销毁了(比如执行了自定义的销毁方法),导致对object进行的处理结果出现了很大的偏差,程序甚至有可能出现异常而退出。

解决这个问题其实很简单,我们只需要在callback回调中重新通过某种方式获取该对象,检查一下该对象是否已被销毁,即可避免上面描述的问题。但是,当程序中需要很多这样的需求时,并且是一个团队在合作写代码,这样就很难避免上述情况的发生。为了规避定时任务的闭包问题,我写了一个延迟调用类,代码如下:

/**
 * script: delayCall.js
 * description: 延迟调用,规避定时任务的闭包问题
 * authors: alwu007@sina.cn
 * date: 2016-04-19
 */

var util = require('util');
var PQueue = require('./pqueue').PQueue;

/**
 * 延迟调用类
 * @param search_func 对象查找函数
 */
var DelayCall = exports.DelayCall = function(search_func) {
    //延迟调用序号
    this._call_seq = 0;
    //延迟调用映射
    this._call_map = {};
    //延迟调用队列
    this._call_queue = new PQueue(DelayCall.compare);
    //对象查找方法
    this._search_func = search_func;

    //设置间隔定时器。FIXME:可以改为在框架的心跳机制中去执行run方法
    //注:setTimeout不支持修改系统时间
    this._interval_id = setInterval(() => {
        this.run();
    }, 1000);
};

//比较延迟调用
DelayCall.compare = function(call1, call2) {
    var time_diff = call1.exec_time - call2.exec_time;
    if (time_diff) {
        return time_diff;
    } else {
        return call1._call_id - call2._call_id;
    }
};

//延迟调用序号自增
DelayCall.prototype._addSequence = function() {
    return ++ this._call_seq;
};

/**
 * 延迟调用
 * @param id 对象查找方法_search_func根据id查找出调用者对象
 * @param method_name 调用者对象上要延迟调用的方法名
 * @param params 要延迟调用的方法参数
 * @param delay 延迟时间,单位秒
 */
DelayCall.prototype.call = function(id, method_name, params, delay) {
    var call_id = this._addSequence();
    var exec_time = Date.now() + delay * 1000;
    var call_elem = {
        _call_id: call_id,
        id: id,
        method_name: method_name,
        params: params,
        delay: delay,
        exec_time: exec_time,
        _canceled: false,
    };
    this._call_queue.enQueue(call_elem);
    this._call_map[call_id] = call_elem;
    return call_id;
};

//取消延迟调用
DelayCall.prototype.cancelCall = function(call_id) {
    var call_elem = this._call_map[call_id];
    if (call_elem) {
        delete this._call_map[call_id];
        call_elem._canceled = true;
    }
};

//运转一次
DelayCall.prototype.run = function() {
    var now = Date.now();
    var pqueue = this._call_queue;
    var search_func = this._search_func;
    var call_elem = pqueue.getHead();
    while (call_elem) {
        if (call_elem._canceled) {
            pqueue.deQueue();
        } else {
            if (now < call_elem.exec_time) {
                break;
            } else {
                //从队列和映射中删除
                pqueue.deQueue();
                delete this._call_map[call_elem._call_id];
                //执行对象的方法
                var obj = search_func(call_elem.id);
                if (obj && typeof obj[call_elem.method_name] == 'function') {
                    obj[call_elem.method_name](call_elem.params);
                }
            }
        }
        call_elem = pqueue.getHead();
    }
};

PQueue的实现请参考我的饿另一篇博文:用NodeJs实现优先级队列PQueue