解决承诺和处理浏览器事件的时间
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
事件为例,我对其他事件也同样感兴趣,比如click
或popstate
事件。)
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 交错的方式,但他们将保留在微任务队列中。
考虑以下用 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
事件为例,我对其他事件也同样感兴趣,比如click
或popstate
事件。)
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 交错的方式,但他们将保留在微任务队列中。