admin管理员组

文章数量:1332184

Consider the following code written in 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);

In this piece of code, waitForMessage installs an event handler waiting for a message to arrive at the current window. Once it has arrived, the promise returned by waitForMessage is being resolved and the event handler removed.

In loop, a new promise is being generated by waitForMessage as soon as the job enqueued by resolving the previous promise is being run.

Now my question is whether loop may not get all messages posted at window due to a timing problem: If the jobs enqueued by Promise.prototype.resolve are not always being run before any tasks enqueued in the browser's event loop, it may be the case that an message event is begin dispatched at window while there is currently no handler listening for this event.

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?

(I just took the message event as an example, I am equally interested in other events, like click or popstate events.)

P.S.: As this has been asked for several times in the ments below, let me describe what I was hoping for with the above code:

I would like to use ES6 features to avoid having to deal too much with callbacks in my code and to make sure that added event listeners are removed in time to avoid memory leaks. Thus I have written something along these lines:

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)();

With this code, I can do something like:

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

This code does not suffer from the issues my question is about; only one promise is created and the event handler is only attached and removed once. Removal of the event handler is guaranteed when the inner function returns.

It is well known that generators in ES6 can also be used for handling promises (like the asyncio package does in Python 3.4). There is proposal for ES7 to include some sugar for these async functions, namely . I was hoping to use this sugar (which is supported by Traceur at the moment) to sugarize my above loop function. However, the proposed async functions only deal with promises so I tried to rewrite my loop code in a fashion that produces a result of promises, the result of which I posted at the beginning of my question.

Consider the following code written in 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);

In this piece of code, waitForMessage installs an event handler waiting for a message to arrive at the current window. Once it has arrived, the promise returned by waitForMessage is being resolved and the event handler removed.

In loop, a new promise is being generated by waitForMessage as soon as the job enqueued by resolving the previous promise is being run.

Now my question is whether loop may not get all messages posted at window due to a timing problem: If the jobs enqueued by Promise.prototype.resolve are not always being run before any tasks enqueued in the browser's event loop, it may be the case that an message event is begin dispatched at window while there is currently no handler listening for this event.

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?

(I just took the message event as an example, I am equally interested in other events, like click or popstate events.)

P.S.: As this has been asked for several times in the ments below, let me describe what I was hoping for with the above code:

I would like to use ES6 features to avoid having to deal too much with callbacks in my code and to make sure that added event listeners are removed in time to avoid memory leaks. Thus I have written something along these lines:

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)();

With this code, I can do something like:

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

This code does not suffer from the issues my question is about; only one promise is created and the event handler is only attached and removed once. Removal of the event handler is guaranteed when the inner function returns.

It is well known that generators in ES6 can also be used for handling promises (like the asyncio package does in Python 3.4). There is proposal for ES7 to include some sugar for these async functions, namely https://github./lukehoban/ecmascript-asyncawait. I was hoping to use this sugar (which is supported by Traceur at the moment) to sugarize my above loop function. However, the proposed async functions only deal with promises so I tried to rewrite my loop code in a fashion that produces a result of promises, the result of which I posted at the beginning of my question.

Share Improve this question edited Jan 26, 2015 at 11:36 Marc asked Jan 26, 2015 at 7:45 MarcMarc 4,4874 gold badges35 silver badges51 bronze badges 8
  • 2 You're kind of bastardizing promises on this one. Promises should not really be used for events that can happen more than once. – Madara's Ghost Commented Jan 26, 2015 at 7:56
  • 1 I fail to see the point why promises should not be used in this case. In any case, the question remains. – Marc Commented Jan 26, 2015 at 8:04
  • 1 Well, because a Promise is an object that represents some data that will be available in the future, like the result of an asynchronous function call. It's not suited for something that may or may not ever arrive, or something that would arrive more than once. – Madara's Ghost Commented Jan 26, 2015 at 8:11
  • 3 @Marc what advantage does it offer over putting it in an event handler without involving a promise? – Benjamin Gruenbaum Commented Jan 26, 2015 at 8:28
  • 1 Whatever other issues there are with this approach, I think you need to say function loop(event) { return waitForMessage().then(loop); }. Note the return. – user663031 Commented Jan 26, 2015 at 9:03
 |  Show 3 more ments

3 Answers 3

Reset to default 4

Addressing your specific issue

The behavior of the promise constructor is well defined both in ES6 promises and promise implementations that implement the constructor spec (virtually everything except old 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

This is well specified and by design. The use case you are describing is mostly why this behavior was guaranteed to work in the spec.

Note that while the promise constructor is specified to run synchronously you still have a race condition with the then - http://jsfiddle/vko4p6zz/

I don't think promises are the correct abstraction here (see jfriend00's answer) but it might make sense with more context - you can rely on the execution order of the promise constructor. You can see this in the specification - new Promise then calls InitializePromise which in turn synchronously calls the function passed.

A possibly better approach.

Just like a promise represents a single value + time, there is an abstraction called an observable that represents multiple values + time. Just like a promise is a functional callback an observable is a functional event emitter. Here is an example using one library (RxJS) - there are several other libraries implementing the concept:

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

In addition to unwrapping with subscribe - you can also map events, filter them, flatMap them and more - they pose just like promises and are as close as I think you can/should get to promises in this context.

At best, your current approach would be relying on a precise and consistent implementation of promise .then() handlers such they never allowed other queued events to process before they were called.

At worst, you would definitely have a chance at missing events.

If you look at Benjamin's jsFiddle and run it in both Chrome and Firefox, you will see that Firefox misses an event (I don't see a missing event in Chrome).

What is clear is that your current design is simply not a safe design because it relies on implementation details (that may or may not be well specified and may or may not be implemented perfectly even if specified) that your code simply does not need to depend on. Whether or not some spec says that this might or should work, this is a brittle design that does not need to be susceptible to this issue.

What would make more sense is to base your design on a constantly installed event listener so there is no way you can ever miss an event. It is probably still possible to use promises with that type of design, but as others have pointed out that is rarely (if ever) a preferred design pattern because promises are not designed for repeated events so you have to continue to create new promises after each event and you will generally find that just using a classic callback for an event handler is a much cleaner way of doing things and takes none of the risks that your current approach is taking.

For example, your proposed code could simply be replaced with this:

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

which is simpler and is guaranteed to not have any vulnerability to missing messages that your code might have. This code is also more consistent with general design patterns for event driven code in mon use for a wide variety of events (such as click events that you mentioned).

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?

  • Promises are run in the micro task queue.
  • UI Events are run in the macro task queue.

The HTML5 spec mandates that the micro task queue is fully drained before the macro task queue starts its next task.

The DOM spec is currently undergoing changes because they want to refine how observers interleave with promises, but they will remain on the micro task queue.

本文标签: javascriptTiming of resolving of promises and handling browser eventsStack Overflow