代码改变世界

【JavaScript】让事件支持先发布后订阅

2017-05-06 09:04  stoneniqiu  阅读(...)  评论(...编辑  收藏

之前写过一个的事件管理器,就是普通的先订阅后发布模式。但实际场景中我们需要做到后订阅的也能收到发布的消息。比如我们关注微信公众号,还是能看到历史消息的。类似于qq离线消息,我先发给你,你登录了就能收到了。就是确保订阅该事件的方法都能被执行

 var eventManger = {
        cached: {},
        handlers: {},
        //类型,绑定事件 
        addHandler: function (type, handler) {
            if (typeof handler !== "function") return;

            if (typeof this.handlers[type] == "undefined") {
                this.handlers[type] = [];
            }
            this.handlers[type].push(handler);

            if (this.cached[type] instanceof Array) {
                //说明有缓存的 可以执行
                handler.apply(null, this.cached[type]);
            }
        },
        removeHandler: function (type, handler) {
            var events = this.handlers[type];
            for (var i = 0, len = events.length; i < len; i++) {
                if (events[i] == handler) {
                    events.splice(i, 1);
                    break;
                }
            }
        },
        trigger: function (type) {
            //如果有订阅的事件,这个时候就触发了
            if (this.handlers[type] instanceof Array) {
                var handlers = this.handlers[type];
                var args = Array.prototype.slice.call(arguments, 1);
                for (var i = 0, len = handlers.length; i < len; i++) {
                    handlers[i].apply(null, args);
                }
            }
            //默认缓存
            this.cached[type] = Array.prototype.slice.call(arguments, 1);
        }
    };

其实就是增加了几行代码。缓存下最后一次触发的时的参数。 然后在addhandle的时候进行判断,如果订阅的时候已经有缓存的参数了,说明该方法可以执行了。

eventManger.addHandler("test", function (res) {
    console.log("先订阅,后发布1", res);
})

eventManger.trigger("test", 2);


eventManger.addHandler("test", function (res) {
    console.log("先发布,后订阅2", res);
})

eventManger.addHandler("test", function (res) {
    console.log("先发布,后订阅3", res);
})

我实际的场景是这样的A事件触发之后,才能执行B方法。但B方法需要在C方法完成之后。也就是B依赖于A和C的完成。且A几乎每次都会很快触发,当然可以设两个个开关变量和一个代理函数,等两个事件都完成之后再do B。代码如下:

var aReady = false;
var cReady = false;
eventManger.addHandler("A", function () {
    aReady = true;
    console.log("do A");
    proxyC();
});

eventManger.trigger("A", 2);

function doB() {
    console.log("do B");
    //实际B中的方法需要在A事件成功之后才能执行
}

function doC() {
    console.log("do C");
    cReady = true;
    proxyC();
}

function proxyC() {
    aReady && cReady && doB();
}
doC();

这样功能是实现了,但是可读性差了,而且事件订阅必须要对位置,如果在trigger之前,doB就永远执行不了,而且代码上多了两个变量和一个方法,最傻的是用一个变量加setTimeout去判断状态,这就可能陷入死循环。

var aReady = false;
eventManger.addHandler("A", function () {
    aReady = true;
    console.log("do A");
});


function doB() {
    console.log("do B");
    //实际B中的方法需要在A事件成功之后才能执行
}

function doC() {
    console.log("do C");
    if (!aReady) {
        console.log("wating...");
        setTimeout(doC, 50);
        return;
    }
    doB();
}

doC();

eventManger.trigger("A", 2);//模拟A事件触发迟

这种办法最不可取吧。因为外部事件可能挂掉,这儿就走不出去了。等于是挖了个坑。但如果事件支持先发布,后订阅,问题就简单了:

eventManger.trigger("A", 2);

function doB() {
    console.log("do B");
    //实际B中的方法需要在A事件成功之后才能执行
}

function doC() {
    console.log("do c");
    eventManger.addHandler("A", function () {
        console.log("do a");
        doB();
    });
}
doC();

 

这样就清晰了很多。事件订阅也不必那么在意调用的位置了。以上只是记住最近的一次的调用参数,可以用于后订阅的事件触发。这适合一次性事件(一个周期只会触发一次的事件)。如果是像推送消息的事件,会不断的触发,如果想要确保也能获得全部的历史记录,就需要记住所有的参数。这是一种情况;实际可能还会有更多的流程依赖,当然对于流程控制有很多办法,也有很多库支持。比如promise和async。本文只是阐述了一个事件和方法的流程相关场景,也许对你有启发。