返回新的 Promise 和 Promise.resolve 之间的区别

Difference between returning new Promise and Promise.resolve

对于下面的代码片段,我想了解 NodeJS 运行时如何处理事情:

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 ***`);

第一种方法:

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

第二种方法:

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.

第三种方法:

Third approach longRunningTaskPromResolve() works.

这是我的理解:

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

我有点不相信我找到的任何资源和我的理解。

所有这三个选项 运行 主线程中的代码并阻止事件循环。当他们开始 运行 执行 while 循环代码时和他们阻止事件循环时的时间略有不同,这将导致他们 运行 与您的某些人的时间有所不同控制台消息。

第一个和第二个选项立即阻止事件循环。

第三个选项阻止事件循环从下一个 tick 开始 - 即 Promise.resolve().then() 调用您传递给 .then() 的回调(在下一个 tick)。


第一个选项只是纯同步代码。毫不奇怪,它会立即阻塞事件循环,直到 while 循环完成。

在第二个选项中,新的 Promise 执行器回调函数也被同步调用,因此它再次立即阻止事件循环,直到 while 循环完成。

在第三个选项中,它调用:

Promise.resolve().then(yourCallback);

Promise.resolve() 创建一个已解决的承诺,然后调用 .then(yourCallback) 该新承诺。这会在事件循环的下一次滴答中安排 yourCallback 到 运行。根据承诺规范,.then() 处理程序始终在事件循环的未来滴答中 运行,即使承诺已经解决。

同时,在此之后的任何其他 Javascript 继续 运行 并且只有当 Javascript 完成后,解释器才会进入事件循环的下一个滴答声并且 运行 yourCallback。但是,当它执行 运行 回调时,它在主线程中 运行 并因此阻塞直到它完成。

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

您示例中的所有代码都在主线程中 运行。 .then() 处理程序被安排到 运行 在事件循环的未来滴答中(仍在主线程中)。这个调度使用了一个微任务队列,它允许它在事件队列中排在其他一些事情的前面,但它仍然在主线程中 运行s 并且在未来的滴答中它仍然是 运行s事件循环。

此外,短语 "execution of a promise" 有点用词不当。 Promises 是一个通知系统,您可以在将来的某个时候使用 .then().catch().finally() 来安排 运行 回调。所以,一般来说,你不想去想"executing a promise"。您的代码执行导致创建一个承诺,然后您根据该承诺发生的情况在将来向 运行 注册该承诺的回调。 Promises 是一个专门的事件通知系统。


Promises 有助于在事情完成时通知您或帮助您安排事情运行。他们不会将任务移动到另一个线程。


作为示例,您可以在第三个选项之后插入一个 setTimeout(fn, 1) 并看到超时从 运行ning 开始被阻止,直到第三个选项完成。这是一个例子。而且,我将阻塞循环的长度都设置为 1000 毫秒,这样您就可以更轻松地看到。 运行 在此处的浏览器中或复制到 node.js 文件中,然后 运行 在那里查看 setTimeout() 如何被 [= 的执行时间阻止按时执行29=]。因此,longRunningTaskPromResolve() 仍然处于阻塞状态。当它到达 运行 时,将它放在 .then() 处理程序中会发生变化,但它仍然处于阻塞状态。

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 ***');

如果您 运行 此代码段并准确查看每条消息的输出时间以及与每条消息关联的时间,您可以看到事情到达 运行 的确切顺序。

这是我在 node.js 中 运行 时的输出(添加的行号有助于下面的解释):

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

这里是一步一步的注释:

  1. 事情开始。
  2. longRunningTask() 已启动。
  3. longRunningTask() 完成。它是完全同步的。
  4. longRunningTaskProm() 已启动。
  5. longRunningTaskProm() 呼叫 resolve()。从这里可以看出,promise 执行器函数(传递给 new Promise(fn)` 的回调也是完全同步的。
  6. longRunningTaskPromResolve() 发起。您可以看到来自 longRunningTaskProm().then(handler) 的处理程序尚未被调用。已安排在事件循环的下一个滴答中 运行,但由于我们还没有回到事件循环,因此尚未调用它。
  7. 我们现在正在设置 1 毫秒计时器。请注意,此计时器在我们启动 longRunningTaskPromResolve() 后仅设置了 1 毫秒。那是因为 longRunningTaskPromResolve() 还没有做太多。它 运行 Promise.resolve().then(handler),但所做的只是在事件循环的未来滴答中将 handler 安排到 运行。所以,只用了 1 毫秒来安排它。该函数较长的 运行ning 部分尚未开始 运行ning。
  8. 我们到达此代码序列的末尾,return 回到事件循环。
  9. 事件循环中安排给 运行 的下一件事是来自 longRunningTaskProm().then(handler) 的处理程序,以便调用它。您可以看到它已经在等待 运行,因为它 运行 在我们 return 进入事件循环后仅 1 毫秒。该处理程序 运行 和我们 return 回到事件循环。
  10. 事件循环中安排到 运行 的下一件事是来自 Promise.resolve().then(handler) 的处理程序,所以我们现在看到它开始到 运行,并且由于它已经排队,它 运行s 在前一个事件结束后立即。
  11. longRunningTaskPromResolve() 到 运行 的循环正好需要 1000 毫秒,然后它 returns 从它的 .then() 处理程序开始,然后调度下一个 .then()该承诺链中的处理程序到 eventl 循环的下一个滴答时 运行。
  12. .then() 达到 运行。
  13. 然后,最后当没有 .then() 处理程序安排到 运行 时,setTimeout() 回调到达 运行。它在 1 毫秒内设置为 运行,但它被所有承诺操作延迟 运行ning 在它之前的更高优先级所以而不是 运行ning 1ms,它 运行 在 1004 毫秒内。