解决承诺和处理浏览器事件的时间

时间:2015-01-26 07:45:21

标签: javascript events promise ecmascript-6

考虑以下用ES6编写的代码:

function waitForMessage() {
    return new Promise((resolve, reject) => {
        function handler(event) {
            resolve(event);
            window.removeEventListener('message', handler);
        };
        window.addEventListener('message', handler); 
    });
}

function loop(event) {
    // do something (synchronous) with event
    waitForMessage().then(loop);
}
waitForMessage().then(loop);

在这段代码中,waitForMessage安装一个事件处理程序,等待消息到达当前窗口。一旦到达,waitForMessage返回的承诺正在解决,并且事件处理程序被删除。

loop中,一旦通过解析先前的承诺而排队的工作正在运行,waitForMessage就会生成新的承诺。

现在我的问题是,由于时间问题,loop是否可能无法在窗口发布所有消息:如果Promise.prototype.resolve排队的作业在浏览器中排队的任何任务之前并不总是被运行&# 39; s事件循环,可能是message事件在window处开始调度,而当前没有处理程序侦听此事件。

标准对这些不同类型的工作/任务的时间安排有什么说法,即解决承诺的回调以及从ES6世界之外调度事件?

(我只是以message事件为例,我对其他事件同样感兴趣,例如clickpopstate事件。)

P.S。:在下面的评论中已经多次询问过这个问题,让我用上面的代码描述一下我希望的内容:

我想使用ES6功能,以避免在代码中处理过多的回调,并确保及时删除添加的事件侦听器以避免内存泄漏。因此,我写了一些东西:

const looper = (element, type, generator) => (... args) => new Promise((resolve, reject) => {
    const iterator = generator(...args);
    const listener = (event) => {
        try {
            let {done, value} = iterator.next(event);
        } catch (error) {
            reject(error);
            element.removeEventListener(type, listener);
        }
        if (done) {
            resolve(value);
            element.removeEventListener(type, listener);
        }  
    }
    element.addEventListener(type, listener);
    listener();
});

const loop = (element, type, generator) => looper(element, type, generator)();

使用此代码,我可以执行以下操作:

loop(window, 'message', function *() {
    event = yield;
    // do something synchronous with event
    if (shouldStopHere) {
        return result;
    }
});

此代码不会受到我的问题所涉及的问题的影响;只创建一个promise,并且只附加和删除一次事件处理程序。当内部函数返回时,保证删除事件处理程序。

众所周知,ES6中的生成器也可用于处理promise(如Python 3.4中的asyncio包)。有人建议ES7为这些异步函数包含一些糖,即https://github.com/lukehoban/ecmascript-asyncawait。我希望使用这种糖(目前由Traceur支持)来糖化我的上述loop功能。但是,建议的异步函数只处理promises,所以我试图以一种产生promises结果的方式重写我的循环代码,结果是我在问题的开头发布的。

3 个答案:

答案 0 :(得分:4)

解决您的具体问题

promise构造函数的行为在实现构造函数规范的ES6 promise和promise实现中得到了很好的定义(几乎除了旧的jQuery之外的所有东西):

var p = new Promise(function(resolve, reject){
     // ALWAYS runs synchronously
     console.log("Hello"); 
}); 
console.log("World"); // program always logs "Hello World", events will never be missed

这是明确指定的设计。您描述的用例主要是为什么这种行为可以保证在规范中起作用。

请注意,虽然指定了构造函数以同步方式运行,但仍然存在then - http://jsfiddle.net/vko4p6zz/

的竞争条件

我不认为promises是正确的抽象(请参阅jfriend00'答案)但是它可能更有意义 - 您可以依赖promise构造函数的执行顺序。您可以看到此in the specification - new Promise然后调用InitializePromise,而同步会调用传递的函数。

可能更好的方法。

就像promise表示单个值+时间一样,有一个名为observable的抽象,表示多个值+时间。就像promise是一个函数回调一样,observable是一个功能事件发射器。以下是使用一个库(RxJS)的示例 - 还有其他几个库实现了这个概念:

var messageStream = Rx.Observable.fromEvent(window, 'message');
messageStream.subscribe(function(value){
   console.log(value); // unwrapping the event
});

除了使用订阅展开外,您还可以map个事件,过滤它们,flatMap它们等等 - 它们就像承诺一样构成,并且尽可能接近我认为你可以/应该接受在这种背景下的承诺。

答案 1 :(得分:3)

充其量,您当前的方法将依赖于promise .then()处理程序的精确且一致的实现,因此它们在调用之前从不允许其他排队事件处理。

最糟糕的是,你肯定有机会错过比赛。

如果您查看Benjamin's jsFiddle并在Chrome和Firefox中运行它,您会看到Firefox错过了一个活动(我在Chrome中看不到丢失的活动)。

显而易见的是,您当前的设计根本不是一个安全的设计,因为它依赖于实现细节(可能会或可能没有明确指定,即使指定可能会也可能不会完美实现)您的代码根本不会需要依靠。无论某些规范是否说这可能或应该有效,这是一个脆弱的设计,不需要容易受到这个问题的影响。

更有意义的是将您的设计基于不断安装的事件监听器,这样您就无法错过任何事件。可能仍然可以使用这种类型的设计的承诺,但正如其他人指出的那样,很少(如果有的话)是一种首选的设计模式,因为承诺不是为重复事件设计的,所以你必须在每次承诺之后继续创造新的承诺。事件,你通常会发现只使用事件处理程序的经典回调是一种更清洁的做事方式,并没有采取当前方法所带来的风险。

例如,您建议的代码可以简单地替换为:

window.addEventListener('message', function(e) {
    // the synchronous code you mentioned to process the event
}); 

更简单,并且保证不会丢失您的代码可能缺少的消息。此代码也与常用于各种事件(例如您提到的点击事件)的事件驱动代码的一般设计模式更加一致。

答案 2 :(得分:3)

  

标准对这些不同类型的工作/任务的时间安排有什么说法,即解决承诺的回调以及从ES6世界之外调度事件?

  • Promise在微任务队列中运行。
  • UI事件在宏任务队列中运行。

HTML5规范规定在宏任务队列开始下一个任务之前micro task queue已完全耗尽。

DOM spec目前是undergoing changes,因为他们希望改进观察者与promises交错的方式,但它们将保留在微任务队列中。