What is the order of execution in javascript promi

2019-01-07 04:53发布

I'd like to explain to myself the execution order of the following snippet that uses javascript promises.

Promise.resolve('A')
  .then(function(a){console.log(2, a); return 'B';})
  .then(function(a){
     Promise.resolve('C')
       .then(function(a){console.log(7, a);})
       .then(function(a){console.log(8, a);});
     console.log(3, a);
     return a;})
  .then(function(a){
     Promise.resolve('D')
       .then(function(a){console.log(9, a);})
       .then(function(a){console.log(10, a);});
     console.log(4, a);})
  .then(function(a){
     console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);

The result is:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

I'm curious about the execution order 1 2 3 7... not the values 'A', 'B'...

My understanding is that if a promise is resolved the 'then' function is put in the browser event queue. So my expectation was 1 2 3 4 ...


@jfriend00 Thanks, thanks a lot for the detailed explanations! It's really an enormous amount of work!

2条回答
Bombasti
2楼-- · 2019-01-07 05:25

The browser's JavaScript engine has something called the "event loop". There is only one thread of JavaScript code running at a time. When a button is clicked or an AJAX request or anything else asynchronous completes, a new event is placed into the event loop. The browser executes these events one at a time.

What you're looking at here is that you run code that executes asynchronously. When the asynchronous code completes, it adds an appropriate event to the event loop. What order the events are added in depends on how long each asynchronous operation takes to complete.

That means that if you're using something like AJAX where you have no control over what order the requests will complete in, your promises can execute in a different order each time.

查看更多
smile是对你的礼貌
3楼-- · 2019-01-07 05:32

Comments

First off, running promises inside of a .then() handler and NOT returning those promises from the .then() callback creates a completely new unattached promise sequence that is not synchronized with the parent promises in any way. Usually, this is a bug and, in fact, some promise engines actually warn when you do that because it is almost never the desired behavior. The only time one would ever want to do that is when you're doing some sort of fire and forget operation where you don't care about errors and you don't care about synchronizing with the rest of the world.

So, all your Promise.resolve() promises inside of .then() handlers create new Promise chains that run independently of the parent chain. You do not have a determinate behavior. It's kind of like launching four ajax calls in parallel. You don't know which one will complete first. Now, since all your code inside those Promise.resolve() handlers happens to be synchronous (since this isn't real world code), then you might get consistent behavior, but that isn't the design point of promises so I wouldn't spend much time trying to figure out which Promise chain that runs synchronous code only is going to finish first. In the real world, it doesn't matter because if order matters, then you won't leave things to chance this way.

Summary

  1. All .then() handlers are called asynchronously after the current thread of execution finishes (as the Promises/A+ spec says, when the JS engine returns back to "platform code"). This is true even for promises that are resolved synchronously such as Promise.resolve().then(...). This is done for programming consistency so that a .then() handler is consistently called asynchronously no matter whether the promise is resolved immediately or later. This prevents some timing bugs and makes it easier for the calling code to see consistent asynchronous execution.

  2. There is no specification that determines the relative order of setTimeout() vs. scheduled .then() handlers if both are queued and ready to run. In your implementation, a pending .then() handler is always run before a pending setTimeout(), but the Promises/A+ spec specification says this is not determinate. It says that .then() handlers can be scheduled a whole bunch of ways, some of which would run before pending setTimeout() calls and some of which might run after pending setTimeout() calls. For example, the Promises/A+ spec allows .then() handlers to be scheduled with either setImmediate() which would run before pending setTimeout() calls or with setTimeout() which would run after pending setTimeout() calls. So, your code should not depend upon that order at all.

  3. Multiple independent Promise chains do not have a predictable order of execution and you cannot rely on any particular order. It's like firing off four ajax calls in parallel where you don't know which one will complete first.

  4. If order of execution is important, do not create a race that is dependent upon minute implementation details. Instead, link promise chains to force a particular execution order.

  5. You generally do not want to create independent promise chains within a .then() handler that are not returned from the handler. This is usually a bug except in rare cases of fire and forget without error handling.

Line By Line Analsysis

So, here's an analysis of your code. I added line numbers and cleaned up the indentation to make it easier to discuss:

1     Promise.resolve('A').then(function (a) {
2         console.log(2, a);
3         return 'B';
4     }).then(function (a) {
5         Promise.resolve('C').then(function (a) {
6             console.log(7, a);
7         }).then(function (a) {
8             console.log(8, a);
9         });
10        console.log(3, a);
11        return a;
12    }).then(function (a) {
13        Promise.resolve('D').then(function (a) {
14            console.log(9, a);
15        }).then(function (a) {
16            console.log(10, a);
17        });
18        console.log(4, a);
19    }).then(function (a) {
20        console.log(5, a);
21    });
22   
23    console.log(1);
24    
25    setTimeout(function () {
26        console.log(6)
27    }, 0);

Line 1 starts a promise chain and attached a .then() handler to it. Since Promise.resolve() resolves immediately, the Promise library will schedule the first .then() handler to run after this thread of Javascript finishes. In Promises/A+ compatible promise libraries, all .then() handlers are called asynchronously after the current thread of execution finishes and when JS goes back to the event loop. This means that any other synchronous code in this thread such as your console.log(1) will run next which is what you see.

All the other .then() handlers at the top level (lines 4, 12, 19) chain after the first one and will run only after the first one gets its turn. They are essentially queued at this point.

Since the setTimeout() is also in this initial thread of execution, it is run and thus a timer is scheduled.

That is the end of the synchronous execution. Now, the JS engine starts running things that are scheduled in the event queue.

As far as I know, there is no guarantee which comes first a setTimeout(fn, 0) or a .then() handler that are both scheduled to run right after this thread of execution. .then() handlers are considered "micro-tasks" so it does not surprise me that they run first before the setTimeout(). But, if you need a particular order, then you should write code that guarantees an order rather than rely on this implementation detail.

Anyway, the .then() handler defined on line 1 runs next. Thus you see the output 2 "A" from that console.log(2, a).

Next, since the previous .then() handler returned a plain value, that promise is considered resolved so the .then() handler defined on line 4 runs. Here's where you're creating another independent promise chain and introducing a behavior that is usually a bug.

Line 5, creates a new Promise chain. It resolves that initial promise and then schedules two .then() handlers to run when the current thread of execution is done. In that current thread of execution is the console.log(3, a) on line 10 so that's why you see that next. Then, this thread of execution finishes and it goes back to the scheduler to see what to run next.

We now have several .then() handlers in the queue waiting to run next. There's the one we just scheduled on line 5 and there's the next one in the higher level chain on line 12. If you had done this on line 5:

return Promise.resolve.then(...)

then you would have linked these promises together and they would be coordinated in sequence. But, by not returning the promise value, you started a whole new promise chain that is not coordinated with the outer, higher level promise. In your particular case, the promise scheduler decides to run the more deeply nested .then() handler next. I don't honestly know if this is by specification, by convention or just an implementation detail of one promise engine vs. the other. I'd say that if the order is critical to you, then you should force an order by linking promises in a specific order rather than rely on who wins the race to run first.

Anyway, in your case, it's a scheduling race and the engine you are running decides to run the inner .then() handler that's defined on line 5 next and thus you see the 7 "C" specified on line 6. It then returns nothing so the resolved value of this promise becomes undefined.

Back in the scheduler, it runs the .then() handler on line 12. This is again a race between that .then() handler and the one on line 7 which is also waiting to run. I don't know why it picks one over the other here other than to say it may be indeterminate or vary per promise engine because the order is not specified by the code. In any case, the .then() handler in line 12 starts to run. That again creates a new independent or unsynchronized promise chain line the previous one. It schedules a .then() handler again and then you get the 4 "B" from the synchronous code in that .then() handler. All synchronous code is done in that handler so now, it goes back to the scheduler for the next task.

Back in the scheduler, it decides to run the .then() handler on line 7 and you get 8 undefined. The promise there is undefined because the previous .then() handler in that chain did not return anything, thus its return value was undefined, thus that is the resolved value of the promise chain at that point.

At this point, the output so far is:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined

Again, all synchronous code is done so it goes back to the scheduler again and it decides to run the .then() handler defined on line 13. That runs and you get the output 9 "D" and then it goes back to the scheduler again.

Consistent with the previously nested Promise.resolve() chain, the the schedule chooses to run the next outer .then() handler defined on line 19. It runs and you get the output 5 undefined. It is again undefined because the previous .then() handler in that chain did not return a value, thus the resolved value of the promise was undefined.

As this point, the output so far is:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined

At this point, there is only one .then() handler scheduled to be run so it runs the one defined on line 15 and you get the output 10 undefined next.

Then, lastly, the setTimeout() gets to run and the final output is:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

If one were to try to predict exactly the order this would run in, then there would be two main questions.

  1. How are pending .then() handlers prioritized vs. setTimeout() calls that are also pending.

  2. How does the promise engine decide to prioritize multiple .then() handlers that are all waiting to run. Per your results with this code it is not FIFO.

For the first question, I don't know if this is per specification or just an implementation choice here in the promise engine/JS engine, but the implementation you reported on appears to prioritize all pending .then() handlers before any setTimeout() calls. Your case is a bit of an odd one because you have no actual async API calls other than specifying .then() handlers. If you had any async operation that actually took any real time to execute at the start of this promise chain, then your setTimeout() would execute before the .then() handler on the real async operation just because the real async operation takes actual time to execute. So, this is a bit of a contrived example and is not the usual design case for real code.

For the second question, I've seen some discussion that discusses how pending .then() handlers at different levels of nesting should be prioritized. I don't know if that discussion was ever resolved in a specification or not. I prefer to code in a way that that level of detail does not matter to me. If I care about the order of my async operations, then I link my promise chains to control the order and this level of implementation detail does not affect me in any way. If I don't care about the order, then I don't care about the order so again that level of implementation detail does not affect me. Even if this was in some specification, it seems like the type of detail that should not be trusted across many different implementations (different browsers, different promise engines) unless you had tested it everywhere you were going to run. So, I'd recommend not relying on a specific order of execution when you have unsynchronized promise chains.


You could make the order 100% determinate by just linking all your promise chains like this (returning inner promises so they are linked into the parent chain):

Promise.resolve('A').then(function (a) {
    console.log(2, a);
    return 'B';
}).then(function (a) {
    var p =  Promise.resolve('C').then(function (a) {
        console.log(7, a);
    }).then(function (a) {
        console.log(8, a);
    });
    console.log(3, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    var p = Promise.resolve('D').then(function (a) {
        console.log(9, a);
    }).then(function (a) {
        console.log(10, a);
    });
    console.log(4, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    console.log(5, a);
});

console.log(1);

setTimeout(function () {
    console.log(6)
}, 0);

This gives the following output in Chrome:

1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6

And, since the promise have all been chained together, the promise order is all defined by the code. The only thing left as an implementation detail is the timing of the setTimeout() which, as in your example, comes last, after all pending .then() handlers.

Edit:

Upon examination of the Promises/A+ specification, we find this:

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

....

3.1 Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

This says that .then() handlers must execute asynchronously after the call stack returns to platform code, but leaves it entirely to the implementation how exactly to do that whether it's done with a macro-task like setTimeout() or micro-task like process.nextTick(). So, per this specification, it is not determinate and should not be relied upon.

I find no information about macro-tasks, micro-tasks or the timing of promise .then() handlers in relation to setTimeout() in the ES6 specification. This is perhaps not surprising since setTimeout() itself is not part of the ES6 specification (it is a host environment function, not a language feature).

I haven't found any specifications to back this up, but the answers to this question Difference between microtask and macrotask within an event loop context explain how things tend to work in browsers with macro-tasks and micro-tasks.

FYI, if you want more info on micro-tasks and macro-tasks, here's an interesting reference article on the topic: Tasks, microtasks, queues and schedules.

查看更多
登录 后发表回答