Разница между возвратом нового Promise и Promise.resolve - PullRequest
8 голосов
/ 06 мая 2020

Для приведенного ниже фрагмента кода я хотел бы понять, как 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 ***`);

1-й подход:

longRunningTask () * Функция 1008 * заблокирует основной поток, как и ожидалось.

2-й подход:

В longRunningTaskProm () обертывание того же кода в обещание , ожидал, что выполнение перейдет от основного потока и будет выполняться как микрозадача. Не похоже, хотел бы понять, что происходит за кулисами.

3-й подход:

Третий подход longRunningTaskPromResolve () работает.

Вот мое понимание:

Создание и выполнение обещания все еще привязано к основному потоку. Только выполнение с разрешением Promise перемещается как микрозадача.

Меня не убедили какие-либо ресурсы, которые я нашел, и мое понимание.

1 Ответ

6 голосов
/ 06 мая 2020

Все три из этих параметров запускают код в основном потоке и блокируют событие l oop. Существует небольшая разница во времени, когда они запускают код while l oop и когда они блокируют событие l oop, что приведет к разнице в том, когда они запускаются по сравнению с некоторыми из ваших сообщений консоли.

Первая и вторая опции блокируют событие l oop немедленно.

Третья опция блокирует событие l oop, начиная со следующего тика - это когда Promise.resolve().then() вызывает переданный вами обратный вызов на .then() (на следующем тике).


Первый вариант - это просто чистый синхронный код. Неудивительно, что он немедленно блокирует событие l oop до тех пор, пока не будет выполнено while l oop.

Во втором варианте новая функция обратного вызова исполнителя Promise также вызывается синхронно, поэтому снова блокирует событие l oop немедленно, пока не будет выполнено while l oop.

В третьем варианте он вызывает:

Promise.resolve().then(yourCallback);

Promise.resolve() создает уже выполненное обещание и затем вызывает .then(yourCallback) для этого нового обещания. Это планирует запуск yourCallback на следующем тике события l oop. Согласно спецификации обещания, обработчики .then() всегда запускаются в будущем тике события l oop, даже если обещание уже выполнено.

Между тем, любые другие Javascript сразу после этого продолжают run, и только после этого Javascript интерпретатор переходит к следующему тику события l oop и запускает yourCallback. Но когда он запускает этот обратный вызов, он выполняется в основном потоке и поэтому блокируется, пока не будет выполнен.

Создание и выполнение обещания все еще привязано к основному потоку. Только выполнение с разрешением Promise перемещается как микрозадача.

Весь ваш код в вашем примере выполняется в основном потоке. Обработчик .then() запланирован для запуска в будущем тике события l oop (все еще в основном потоке). В этом планировании используется очередь микрозадач, которая позволяет ему ставить перед некоторыми другими вещами в очереди событий, но оно по-прежнему выполняется в основном потоке и по-прежнему выполняется в будущем тике события l oop.

Кроме того, фраза «выполнение обещания» немного неверна. Обещания - это система уведомлений, и вы планируете запускать обратные вызовы с ними в какой-то момент в будущем, используя .then(), .catch() или .finally() в обещании. Итак, в общем, вы не хотите думать о «выполнении обещания». Ваш код выполняется, вызывая создание обещания, а затем вы регистрируете обратные вызовы для этого обещания для запуска в будущем в зависимости от того, что происходит с этим обещанием. Обещания - это специализированная система уведомлений о событиях.


Обещания помогают уведомить вас, когда что-то завершено, или помочь вам запланировать время выполнения. Они не перемещают задачи в другой поток.


В качестве иллюстрации вы можете вставить setTimeout(fn, 1) сразу после третьего варианта и увидеть, что выполнение тайм-аута заблокировано до завершения третьего варианта. Вот пример этого. И я сделал все блокирующие петли длиной 1000 мс, чтобы вам было легче видеть. Запустите это в браузере здесь или скопируйте в файл node.js и запустите его там, чтобы увидеть, как setTimeout() блокируется от своевременного выполнения на время выполнения longRunningTaskPromResolve(). Итак, 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(). Из этого видно, что функция исполнителя обещания (обратный вызов, переданный в new Promise (fn) `, также полностью синхронен.
  6. longRunningTaskPromResolve() инициировано. Вы можете видеть, что обработчик из longRunningTaskProm().then(handler) еще не Это было запланировано для выполнения на следующем тике события l oop, но, поскольку мы еще не вернулись к событию l oop, оно еще не было вызвано.
  7. Сейчас мы устанавливаем таймер 1 мс. Обратите внимание, что этот таймер устанавливается только через 1 мс после того, как мы запустили longRunningTaskPromResolve(). Это потому, что longRunningTaskPromResolve() пока мало что сделал. Он запустил Promise.resolve().then(handler), но все, что было сделано, было запланируйте запуск handler в будущем тике события l oop. Итак, это заняло всего 1 мс, чтобы запланировать это. Долговременная часть этой функции еще не запущена.
  8. Мы перейти к концу этой последовательности кода и вернуться к событию l oop.
  9. Следующее, что запланировано для запуска в событии l oop, - это обработчик из longRunningTaskProm().then(handler), чтобы он был вызван . Видно, что он уже ждал, чтобы запустить sin ce он проработал всего 1 мс после того, как мы вернулись к событию l oop. Этот обработчик запускается, и мы возвращаемся к событию l oop.
  10. Следующее, что запланировано для запуска в событии l oop, - это обработчик из Promise.resolve().then(handler), поэтому теперь мы видим, что он начинает выполняться. и поскольку он уже был поставлен в очередь, он запускается сразу после завершения предыдущего события.
  11. Для запуска l oop in longRunningTaskPromResolve() требуется ровно 1000 мсек, а затем он возвращается из своего .then() обработчика, который планирует затем следующий обработчик .then() в этой цепочке обещаний для запуска на следующем тике события l oop.
  12. Этот .then() запускается.
  13. Затем, наконец, когда есть нет запланированных обработчиков .then(), запускается обратный вызов setTimeout(). Он был настроен на выполнение за 1 мс, но все действия обещания, выполнявшиеся с более высоким приоритетом перед ним, были отложены, поэтому вместо выполнения 1 мс он выполнялся за 1004 мс.
...