Difference between returning new Promise and Promi

2020-07-09 07:06发布

问题:

For the below code snippet, i would like to understand how NodeJS runtime handles things :

const billion = 1000000000;

function longRunningTask(){
    let i = 0;
    while (i <= billion) i++;

    console.log(`Billion loops done.`);
}

function longRunningTaskProm(){
    return new Promise((resolve, reject) => {
        let i = 0;
        while (i <= billion) i++;

        resolve(`Billion loops done : with promise.`);
    });
}

function longRunningTaskPromResolve(){
    return Promise.resolve().then(v => {
        let i = 0;
        while (i <= billion) i++;

        return `Billion loops done : with promise.resolve`;
    })
}


console.log(`*** STARTING ***`);

console.log(`1> Long Running Task`);
longRunningTask();

console.log(`2> Long Running Task that returns promise`);
longRunningTaskProm().then(console.log);

console.log(`3> Long Running Task that returns promise.resolve`);
longRunningTaskPromResolve().then(console.log);

console.log(`*** COMPLETED ***`);

1st approach :

longRunningTask() function will block the main thread, as expected.

2nd approach :

In longRunningTaskProm() wrapping the same code in a Promise, was expecting execution will move away from main thread and run as a micro-task. Doesn't seem so, would like to understand what's happening behind the scenes.

3rd approach :

Third approach longRunningTaskPromResolve() works.

Here's my understanding :

Creation and execution of a Promise is still hooked to the main thread. Only Promise resolved execution is moved as a micro-task.

Am kinda not convinced with whatever resources i found & with my understanding.

回答1:

All three of these options run the code in the main thread and block the event loop. There is a slight difference in timing for WHEN they start running the while loop code and when they block the event loop which will lead to a difference in when they run versus some of your console messages.

The first and second options block the event loop immediately.

The third option blocks the event loop starting on the next tick - that's when Promise.resolve().then() calls the callback you pass to .then() (on the next tick).


The first option is just pure synchronous code. No surprise that it immediately blocks the event loop until the while loop is done.

In the second option the new Promise executor callback function is also called synchronously so again it blocks the event loop immediately until the while loop is done.

In the third option, it calls:

Promise.resolve().then(yourCallback);

The Promise.resolve() creates an already resolved promise and then calls .then(yourCallback) on that new promise. This schedules yourCallback to run on the next tick of the event loop. Per the promise specification, .then() handlers are always run on a future tick of the event loop, even if the promise is already resolved.

Meanwhile, any other Javascript right after this continues to run and only when that Javascript is done does the interpreter get to the next tick of the event loop and run yourCallback. But, when it does run that callback, it's run in the main thread and therefore blocks until it's done.

Creation and execution of a Promise is still hooked to the main thread. Only Promise resolved execution is moved as a micro-task.

All your code in your example is run in the main thread. A .then() handler is scheduled to run in a future tick of the event loop (still in the main thread). This scheduling uses a micro task queue which allows it to get in front of some other things in the event queue, but it still runs in the main thread and it still runs on a future tick of the event loop.

Also, the phrase "execution of a promise" is a bit of a misnomer. Promises are a notification system and you schedule to run callbacks with them at some point in the future using .then() or .catch() or .finally() on a promise. So, in general, you don't want to think of "executing a promise". Your code executes causing a promise to get created and then you register callbacks on that promise to run in the future based on what happens with that promise. Promises are a specialized event notification system.


Promises help notify you when things complete or help you schedule when things run. They don't move tasks to another thread.


As an illustration, you can insert a setTimeout(fn, 1) right after the third option and see that the timeout is blocked from running until the third option finishes. Here's an example of that. And, I've made the blocking loops all be 1000ms long so you can more easily see. Run this in the browser here or copy into a node.js file and run it there to see how the setTimeout() is blocked from executing on time by the execution time of longRunningTaskPromResolve(). So, longRunningTaskPromResolve() is still blocking. Putting it inside a .then() handler changes when it gets to run, but it is still blocking.

const loopTime = 1000;

let startTime;
function log(...args) {
    if (!startTime) {
        startTime = Date.now();
    }
    let delta = (Date.now() - startTime) / 1000;
    args.unshift(delta.toFixed(3) + ":");
    console.log(...args);
}

function longRunningTask(){
    log('longRunningTask() starting');
    let start = Date.now();
    while (Date.now() - start < loopTime) {}

    log('** longRunningTask() done **');
}

function longRunningTaskProm(){
    log('longRunningTaskProm() starting');
    return new Promise((resolve, reject) => {
        let start = Date.now();
        while (Date.now() - start < loopTime) {}
        log('About to call resolve() in longRunningTaskProm()');
        resolve('** longRunningTaskProm().then(handler) called **');
    });
}

function longRunningTaskPromResolve(){
    log('longRunningTaskPromResolve() starting');
    return Promise.resolve().then(v => {
        log('Start running .then() handler in longRunningTaskPromResolve()');
        let start = Date.now();
        while (Date.now() - start < loopTime) {}
        log('About to return from .then() in longRunningTaskPromResolve()');
        return '** longRunningTaskPromResolve().then(handler) called **';
    })
}


log('*** STARTING ***');

longRunningTask();

longRunningTaskProm().then(log);

longRunningTaskPromResolve().then(log);

log('Scheduling 1ms setTimeout')
setTimeout(() => {
    log('1ms setTimeout Got to Run');
}, 1);

log('*** First sequence of code completed, returning to event loop ***');

If you run this snippet and look at exactly when each message is output and the timing associated with each message, you can see the exact sequence of when things get to run.

Here's the output when I run it in node.js (line numbers added to help with the explanation below):

1    0.000: *** STARTING ***
2    0.005: longRunningTask() starting
3    1.006: ** longRunningTask() done **
4    1.006: longRunningTaskProm() starting
5    2.007: About to call resolve() in longRunningTaskProm()
6    2.007: longRunningTaskPromResolve() starting
7    2.008: Scheduling 1ms setTimeout
8    2.009: *** First sequence of code completed, returning to event loop ***
9    2.010: ** longRunningTaskProm().then(handler) called **
10   2.010: Start running .then() handler in longRunningTaskPromResolve()
11   3.010: About to return from .then() in longRunningTaskPromResolve()
12   3.010: ** longRunningTaskPromResolve().then(handler) called **
13   3.012: 1ms setTimeout Got to Run

Here's a step-by-step annotation:

  1. Things start.
  2. longRunningTask() initiated.
  3. longRunningTask() completes. It is entirely synchronous.
  4. longRunningTaskProm() initiated.
  5. longRunningTaskProm() calls resolve(). You can see from this that the promise executor function (the callback passed to new Promise(fn)` is entirely synchronous too.
  6. longRunningTaskPromResolve() initiated. You can see that the handler from longRunningTaskProm().then(handler) has not yet been called. That has been scheduled to run on the next tick of the event loop, but since we haven't gotten back to the event loop yet, it hasn't yet been called.
  7. We're now setting the 1ms timer. Note that this timer is being set only 1ms after we started longRunningTaskPromResolve(). That's because longRunningTaskPromResolve() didn't do much yet. It ran Promise.resolve().then(handler), but all that did was schedule the handler to run on a future tick of the event loop. So, that only took 1ms to schedule that. The long running part of that function hasn't started running yet.
  8. We get to the end of this sequence of code and return back to the event loop.
  9. The next thing scheduled to run in the event loop is the handler from longRunningTaskProm().then(handler) so that gets called. You can see that it was already waiting to run since it ran only 1ms after we returned to the event loop. That handler runs and we return back to the event loop.
  10. The next thing scheduled to run in the event loop is the handler from Promise.resolve().then(handler) so we now see that that starts to run and since it was already queued, it runs immediately after the previous event finished.
  11. It takes exactly 1000ms for the loop in longRunningTaskPromResolve() to run and then it returns from it's .then() handler which schedules then next .then() handler in that promise chain to run on the next tick of the eventl loop.
  12. That .then() gets to run.
  13. Then, finally when there are no .then() handlers scheduled to run, the setTimeout() callback gets to run. It was set to run in 1ms, but it got delayed by all the promise action running at a higher priority ahead of it so instead of running 1ms, it ran in 1004ms.