Как подождать завершения setTimeout с async/await

Есть простая как грабли задача. Три операции - две синхронные, между ними одна асинхронная. Нужно чтобы асинхронная задача выполнилась второй по счёту, а не последней. Конечный результат вычислений должен быть равен единице - 1. Как этого достичь используя синтаксис async/await, не используя глобальный объект Promise. Ниже примерное решение, НО неверное.

Зачем мне это нужно, что это за странный код, какой в этом смысл и т.д. эти вопросы сразу мимо. Я потратил на эту задачу несколько дней и все к кому я обращался не смогли её решить. Только ради этой задачи я зарегался на этом сайте, так что для меня это важно.

const summarize = (num1, num2) => num1 + num2;
const divide = (num1, num2) => num1/num2;

let x = 2;
let y = 6;
let z = 10;

const calcAsync = async () => {
  x = summarize(x, x);                           // (1шаг) 2 + 2 = 4
  x = await setTimeout(() => summarize(x, y), 1);// (2шаг) ??? должно получится 4 + 6 = 10
  x = divide(x, z);                              // (3шаг) ??? должно получится 10/10 = 1
}

calcAsync();

Ответы (3 шт):

Автор решения: EzioMercer

Вам надо было обернуть setTimeout в Promise:

const summarize = (num1, num2) => num1 + num2;
const divide = (num1, num2) => num1 / num2;

let x = 2;
let y = 6;
let z = 10;

const calcAsync = async() => {
  x = summarize(x, x);
  
  console.log(1, x);
  
  x = await new Promise((resolve, reject) => {
    setTimeout(() => resolve(summarize(x, y)));
  });
  
  console.log(2, x);
  
  x = divide(x, z);
  
  console.log(3, x);
}

calcAsync();

Объяснение того, почему нельзя обойтись без Promise:

P.S. Формально это утверждение - не верное. Как показал @Grundy достаточно заменить Promise на самый обычный thenable объект (объект у которого есть метод then) и в этом случае всё будет работать точно так же как и с Promise. Но этот ответ даст вам понимание того, почему не поможет await-тить или оборачивать в async ф-ию setTimeout

Рассмотрим этот кусок кода:

const calcAsync = async () => {
  x = summarize(x, x);
  x = await setTimeout(() => summarize(x, y), 1);
  x = divide(x, z);
}

Т.к. async/await возвращает и работает Promise, то давайте переведём этот кусок кода на язык Promise-ов:

const calcAsync = () => {
  return Promise.resolve()
    .then(function () {
      x = summarize(x, x);

      return setTimeout(() => summarize(x, y), 1);
    })
    .then(function (handleResolved) {
      x = handleResolved;
      x = divide(x, z);
    });
};

И так нас интересует строка с setTimeout. В документации сказано, что then на вход получит от Promise-а 2 аргумента - это handleResolved и handleRejected и так же сказано, что он сам тоже вернёт Promise. Нас интересует, только handleResolved, потому дальше handleRejected будет опущен. И так второй then на вход получит возращаемое значение setTimeout - это число timeoutID и абсолютно не важно что какая ф-ия будет запущена и когда она будет запущена. Следовательно во втором then, где строчка x = handleResolved в x будет записано число timeoutID и дальнейшие вычисления будут проводиться именно с этим значением. Что важно это число каждый раз будет разным, и перезапуская один и тот же код в одной сессии вы будете получать разные значения

Демо код (специально убрал x = await..., чтобы не подумали, что всё из-за того что в x что-то записывается):

const summarize = (num1, num2) => num1 + num2;
const divide = (num1, num2) => num1 / num2;

let x = 2;
let y = 6;
let z = 10;

const calcAsync = async() => {
  x = summarize(x, x);

  console.log(await setTimeout(() => summarize(x, y)));
  console.log(await setTimeout(() => summarize(x, y)));
  console.log(await setTimeout(() => summarize(x, y)));
  console.log(await setTimeout(() => summarize(x, y)));

  x = divide(x, z);
}

calcAsync();

Нам же надо чтобы в x во втором then попало значение summarize(x, y).

Автором был предложен вариант, обернуть setTimeout в async ф-ию т.к. async нам возвращает Promise. Замечу, что писать в обёртке что-то типа return await setTimeout будет так же бесполезно как и если без обёртки. Давайте посмотрим, что будет в этом случае:

const asyncSum = async () => {return setTimeout(() => summarize(x, y), 1)}

const calcAsync = async() => {
  x = summarize(x, x);
  
  x = await asyncSum();
  
  x = divide(x, z);
}

переписвая это на язык Promise-ов получим:

const asyncSum = () => {
  return Promise.resolve().then(function () {
    return setTimeout(() => summarize(x, y), 1);
  });
};

const calcAsync = () => {
  return Promise.resolve()
    .then(function () {
      x = summarize(x, x);

      return asyncSum();
    })
    .then(function (handleResolved) {
      x = handleResolved;
      x = divide(x, z);
    });
};

Теперь можем спокойно подставить результат нашей ф-ии asyncSum туда, где он вызывается и получаем:

const calcAsync = () => {
  return Promise.resolve()
    .then(function () {
      x = summarize(x, x);

      return Promise.resolve().then(function () {
        return setTimeout(() => summarize(x, y), 1);
      });
    })
    .then(function (handleResolved) {
      x = handleResolved;
      x = divide(x, z);
    });
};

И что мы видим? Мы опять отправляем в handleResolved наш timeoutID. Потому обёртки тут тоже не помогут отправить само значение summarize(x, y)

Теперь, когда разобрались, почему мы не можем ни await-тить ни оборачивать в async ф-ии наш setTimeout, посмотрим как нам помогает обёртка Promise-ом:

const calcAsync = async() => {
  x = summarize(x, x);
  
  x = await new Promise((resolve) => {
    setTimeout(() => resolve(summarize(x, y)), 1);
  });
  
  x = divide(x, z);
}

опять переписываем на язык Promise-ов и получаем:

const calcAsync = () => {
  return Promise.resolve()
    .then(function () {
      x = summarize(x, x);

      return new Promise((resolve) => {
        setTimeout(() => resolve(summarize(x, y)), 1);
      });
    })
    .then(function (handleResolved) {
      x = handleResolved;
      x = divide(x, z);
    });
};

Теперь видно, что мы явно в handleResolved передаём значение ф-ии summarize(x, y) в строке resolve(summarize(x, y)). Тут уже дело в том, что пока не будет вызыван/отработан resolve последующий then будет ждать, сколько бы это не заняло времени

P.S В документации сказано, что этот кусок кода:

async function foo() {
   return 1
}

похоже на:

function foo() {
   return Promise.resolve(1)
}

но я переписывал это как:

function foo() {
  return Promise.resolve().then(function () {
    return 1;
  });
}

в самих рассуждениях и примерах это ни на что не влияет. Вся логика будет точно такой же, если переписывать как показано в документации, но чтобы не переключаться постоянно между методами resolve и then, решил что, если всегда будет then, то будет меньше путаницы и это позволит лучше понять что происходит. Надеюсь смог добиться этого :)

→ Ссылка
Автор решения: Qwertiy
await setTimeout(() => summarize(x, y), 1);
  1. setTimeout возвращает id таймера сразу, никакого толка от await тут нет.
  2. Да и вообще, результат вызова summarize не используется.
  3. 1ms - это странно. Скорее всего тут и нулю или отсутствующему аргументу неплохо.

Надо так:

x = await new Promise(resolve => setTimeout(resolve, 0, summarize(x, y)))

Хотя в любом случае вся эта конструкция предполагает, что результат известен синхронно, так что непонятно, нафига она сделась.

Нужно только с async/await без Promise, без then(), код должен быть максимально похож на синхронный.

Так не пойдёт: await работает именно с then.

→ Ссылка
Автор решения: Grundy

Можно воспользоваться тем, что await можно применить к thenable объекту, а именно объекту с методом then.

По аналогии с конструктором Promise метод then так же принимает два аргумента для resolve и reject.

const summarize = (num1, num2) => num1 + num2;
const divide = (num1, num2) => num1 / num2;

let x = 2;
let y = 6;
let z = 10;

const calcAsync = async() => {
  x = summarize(x, x); // (1шаг) 2 + 2 = 4
  console.log('(1шаг)', x);
  x = await {
    then(r) {
      setTimeout(() => r(summarize(x, y)), 1)
    }
  }; // (2шаг) ??? должно получится 4 + 6 = 10
  console.log('(2шаг)', x);
  x = divide(x, z); // (3шаг) ??? должно получится 10/10 = 1
  console.log('(3шаг)', x);
}

calcAsync();

→ Ссылка