Timing of resolving of promises and handling brows

2019-05-10 08:18发布

问题:

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 comments 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.com/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.

回答1:

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.net/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 compose just like promises and are as close as I think you can/should get to promises in this context.



回答2:

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 common use for a wide variety of events (such as click events that you mentioned).



回答3:

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.