【问题标题】:Timing of resolving of promises and handling browser events解决承诺和处理浏览器事件的时机
【发布时间】:2015-03-24 15:20:15
【问题描述】:

考虑以下用 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 返回的 Promise 将被解析并移除事件处理程序。

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

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

标准对这些不同类型的作业/任务的时间安排有何规定,即解决 Promise 的回调和来自 ES6 世界之外的事件分派?

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

P.S.:由于在下面的 cmets 中已经多次询问过这个问题,让我用上面的代码描述一下我所希望的:

我想使用 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 函数。然而,提议的异步函数只处理承诺,所以我尝试以一种产生承诺结果的方式重写我的循环代码,我在问题的开头发布了这个结果。

【问题讨论】:

  • 你对这个承诺有点自欺欺人。 Promise 不应该真正用于可能发生多次的事件。
  • 我看不出为什么在这种情况下不应使用承诺。无论如何,问题仍然存在。
  • 好吧,因为 Promise 是一个对象,它代表了一些未来可用的数据,比如异步函数调用的结果。它不适合可能会或可能不会到达的东西,或者会不止一次到达的东西。
  • @Marc 与将其放入事件处理程序而不涉及承诺相比有什么优势?
  • 无论这种方法有什么其他问题,我认为你需要说function loop(event) { return waitForMessage().then(loop); }。注意return

标签: javascript events promise ecmascript-6


【解决方案1】:

解决您的具体问题

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

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

请注意,虽然 promise 构造函数被指定为同步运行,但您仍然会遇到 then - http://jsfiddle.net/vko4p6zz/

的竞争条件

我不认为 promise 在这里是正确的抽象(参见 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
});

除了使用 subscribe 解包 - 您还可以 map 事件、过滤它们、flatMap 它们等等 - 它们就像 Promise 一样组成,并且我认为在这种情况下你可以/应该接近 Promise .

【讨论】:

  • (注意:虽然 promise 构造函数是同步的,但循环中的 then 当然不是 - 值得澄清)
  • 您在规范中谈到了InitializePromise(),但我认为这不是问题所在。我认为问题在于调用resolve() 和调用.then() 处理程序之间的时间。那是未安装事件处理程序的时间,因此如果在调用resolve() 触发的.then() 处理程序被调用之前有可能处理事件,那么这将是错过事件的窗口。这不是操作问题吗?
  • @Marc 我不确定我和 jfriend00 还能强调这个事实 - 在这里,promise 不适合你的问题。你不应该在这种情况下使用它们——我真的不想诉诸我或他的权威,但我们都见过数百个这样的问题,并且可以很好地发现这些案例。有关更一般的推理,请参阅github.com/kriskowal/gtor
  • @BenjaminGruenbaum 我现在明白了。感谢您的所有意见。我已经编辑了我的问题并添加了一些动机,即能够在这种情况下使用提议的 ES7 异步函数。所以我试着把钉子变成螺丝,只是为了能用螺丝刀。
  • @Marc 当 ES7 到来时,它将附带异步生成器和 for... on 循环。不用担心支持即将推出 :) github.com/jhusain/asyncgenerator - 你的 ES7 可能看起来像这样:gist.github.com/benjamingr/e377e2417f803561439b
【解决方案2】:

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

在最坏的情况下,您肯定有机会错过活动。

如果你查看Benjamin's jsFiddle 并在 Chrome 和 Firefox 中运行它,你会看到 Firefox 错过了一个事件(我在 Chrome 中没有看到一个丢失的事件)。

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

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

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

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

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

【讨论】:

  • 那么你是说处理事件和排队promise回调的顺序是未指定的?请注意,我不是在谈论“用户空间”代码中的 Promise 实现,而是在谈论 ES6 中的原生 Promise。
  • @Marc - 我不确切知道在这方面指定了什么或没有指定什么。像本杰明这样的人可以谈论这一点。我的观点是(在我看来)编写需要特定规范的代码以及在所有可能运行您的代码的引擎上对该规范的完美实现是愚蠢的,而这种对实现细节的依赖是完全没有必要的。当您谈论使用新规范的新实现时,这更是一个问题。换句话说,为什么在你不需要的时候编写对这些细节敏感的脆弱代码?
【解决方案3】:

标准对这些不同类型的作业/任务的时间安排有何规定,即解决 Promise 的回调和来自 ES6 世界之外的事件分派?

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

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

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

【讨论】:

  • 这回答了我原来的问题。
猜你喜欢
  • 2022-01-22
  • 2016-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多