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

Timing of resolving of promises and handling browser events

考虑以下用 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 安装了一个等待消息到达当前 window 的事件处理程序。一旦它到达,waitForMessage 返回的承诺将被解析并删除事件处理程序。

loop 中,waitForMessage 正在生成一个新的承诺,只要通过解析先前的承诺而排队的作业正在 运行。

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

标准对这些不同类型 jobs/tasks 的计时有何规定,即解析 promise 的回调和从 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;
    }
});

此代码没有遇到我的问题所涉及的问题;只创建一个承诺,事件处理程序只附加和删除一次。当内部函数 returns.

时,保证移除事件处理程序

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

充其量,您当前的方法将依赖于 promise .then() 处理程序的精确和一致的实现,这样它们就不会允许其他排队的事件在被调用之前进行处理。

最坏的情况下,你肯定有机会错过比赛。

如果您在 Chrome 和 Firefox 中查看 Benjamin's jsFiddle 和 运行,您会看到 Firefox 错过了一个事件(我在 Chrome).

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

更有意义的是将您的设计基于一个不断安装的事件侦听器,这样您就不会错过任何一个事件。可能仍然可以在这种类型的设计中使用 promises,但正如其他人指出的那样,这很少(如果有的话)是首选的设计模式,因为 promises 不是为重复事件设计的,因此您必须在每次事件后继续创建新的 promises事件,您通常会发现,仅对事件处理程序使用经典回调是一种更简洁的处理方式,并且可以承担 none 当前方法所承担的风险。

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

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

它更简单,并且保证不会因您的代码可能存在的丢失消息而存在任何漏洞。此代码也更符合事件驱动代码的一般设计模式,这些代码通常用于各种事件(例如您提到的点击事件)。

解决您的具体问题

promise 构造函数的行为在 ES6 promises 和实现构造函数规范的 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

这是设计使然的。您描述的用例主要是保证此行为在规范中有效的原因。

请注意,虽然将 promise 构造函数同步指定为 运行,但您仍然存在 then - http://jsfiddle.net/vko4p6zz/[ 的竞争条件=19=]

我认为 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 它们等等 - 它们就像承诺一样组合并且与我认为的一样接近 can/should 在这种情况下获得承诺。

What does the standard say about the timing of these different types of jobs/tasks, namely resolving the callbacks of promises and the dispatching of events from outside of the ES6 world?

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

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

DOM spec is currently undergoing changes 因为他们想改进观察者与 promises 交错的方式,但他们将保留在微任务队列中。