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


doALongThing(() => console.log("I will be logged second!"));  
console.log("I will be logged first!");  

К сожалению, это также один из основных недостатков JavaScript. Задача выполнения синхронного кода становится сложнее, так как по умолчанию всё асинхронно.
Первым решением проблемы были колбэки. Если часть нашего кода зависит от какого-то результата, мы должны были вложить наш код —


doSomething((response) => {  
   doSomethingElse(response,(secondResponse) => {
        doAThirdThing(secondResponse);
   });
})

Вложенные колбэки в колбэках, как мы знаем, становятся непригодными. Таким образом, были созданы промисы (Promise). Они позволили нам работать с синхронными кодом более чистым и плоским способом.


doSomething()  
.then((response) => doSomethingElse(response));
.then((secondResponse) => doAThirdThing(secondResponse));

// Even cleaner
doSomething().then(doSomethingElse).then(doAThirdThing);  

Как и всё, промисы тоже не идеальны. Таким образом, в рамках спецификации ES2017 был определен другой метод для работы с синхронным кодом: Асинхронные функции. Это позволяет нам писать асинхронный код, как если бы он был синхронным.


Создание асинхронной функции


Асинхронная функция определяется выражением асинхронной функции. Базовая функция выглядит так:


async function foo() {  
    const value = await somePromise();
    return value;
}

Мы определяем функцию как асинхронную с помощью async. Это ключевое слово может использоваться с любым синтаксисом объявления функции —


// Basic function
async function foo() {  … }

// Arrow function
const foo = async () => { … }

// Class methods
class Bar {  
    async foo() { … }
}

Как только мы определили функцию как асинхронную, мы можем использовать ключевое слово await.
Это ключевое слово помещается перед вызовом промиса, оно приостанавливает выполнение функции до тех пор, пока промис не будет выполнен или отклонён.


Обработка ошибок


Обработка ошибок в асинхронных функциях выполняется с помощью блоков try и catch.
Первый блок (try) позволяет нам попробовать совершить действие. Второй блок (catch), вызывается, если действие не выполняется. Он принимает один параметр, содержащий любую ошибку.


async function foo() {  
    try {
        const value = await somePromise();
        return value;
    }
    catch (err) {
        console.log("Oops, there was an error :(");
    }
}

Использование Асинхронных функций


Асинхронные функции не являются заменой промисов. Они идентичны по своей природе. Так, асинхронная функция ожидает исполнения обещания и всегда возвращает промис.


Промис, возвращаемый асинхронной функцией, будет разрешен с любым значением, возвращаемым функцией.


async function foo() {  
    await somePromise();
    return ‘success!’
}

foo().then((res) => console.log(res)) // ‘success!’  

Если будет выброшена ошибка, промис будет отклонён с этой ошибкой.


async function foo() {  
    await somePromise();
    throw Error(‘oops!’)
}

foo()  
.then((res) => console.log(res))
.catch((err) => console.log(err)) // ‘oops!’

Выполнение Асинхронных функций в параллельно


С промисами мы можем выполнять несколько обещаний параллельно с помощью метода Promise.all ().


function pause500ms() {  
    return new Promise((res) => setTimeout(res, 500));
}

const promise1 = pause500ms();  
const promise2 = pause500ms();

Promise.all([promise1, promise2]).then(() => {  
   console.log("I will be logged after 500ms");
});

С асинхронными функциями нам нужно немного поработать, чтобы получить такой же эффект. Если мы просто перечислим каждую функцию, ожидающую в очереди, они будут выполняться последовательно, так как await приостанавливает выполнение оставшейся части функции.


async function inSequence() {  
    await pause500ms();
    await pause500ms(); 
    console.log("I will be logged after 1000ms");
}

Это займет 1000 мс, так как второе ожидание не запустится, пока не завершится первое. Чтобы обойти это, мы должны ссылаться на функции таким образом:


async function inParallel() {  
    const await1 = await pause500ms();
    const await2 = await pause500ms();
    await await1;
    await await2;
    console.log("I will be logged after 500ms");
}

Теперь это займет всего 500 мс, потому что обе функции pause500ms () выполняются одновременно.


Промисы или Асинхронные функции?


Как я уже упоминала, асинхронные функции не заменяют промисов. Они идентичны по своей природе. Асинхронные функции предоставляют альтернативный, а в некоторых случаях и лучший способ работы с основанными на промисах функциями. Но они всё ещё используют и производят промисы.


Поскольку возвращается промис, асинхронная функция может быть вызвана другой асинхронной функцией или промисом. Мы можем смешивать и сочетать в зависимости от того, какой синтаксис лучше всего подходит для каждого случая.


function baz() {  
    return new Promise((res) => setTimeout(res, 1000));
}

async function foo() {  
    await baz();
    return 'foo complete!';
}

async function bar() {  
    const value = await foo();
    console.log(value);
    return 'bar complete!';
}

bar().then((value) => console.log(value));  

Происходит следующее:


  • ждём 1000мс
  • лог «foo complete!»
  • лог «bar complete!»

Поддержка


На момент написания статьи, асинхронные функции и промисы доступны в текущих версиях всех основных браузеров, за исключением Internet Explorer и Opera Mini.



Поделиться с друзьями
-->

Комментарии (32)


  1. token
    13.04.2017 16:06
    +2

    В чем заключается посыл статьи? Я привел выдержки из общедоступной документации со своими размышлениями на тему? И еще непонятно в тексте есть «Как я уже упоминала», а автор вроде как мальчик. И что такое 101?
    upd: вчитался, реально походу какую то статью запихнули в переводчик…


    1. Suvitruf
      13.04.2017 16:44
      +2

      Автор оригинала — девушка.


    1. ellrion
      13.04.2017 18:31
      +1

      101 обозначает базовый или вводный урок (an introductory lesson on something).


  1. Kot_DaVinchi
    13.04.2017 16:40
    +3

    Тут ошибка(и в оригинальной статье, кстати, тоже).
    Функция inParallel выполняется последовательно(за 1 секунду). Чтобы она выполнилась параллельно нужно убрать await перед вызовом функций:

    async function inParallel() {
        const await1 = pause500ms();
        const await2 = pause500ms();
        await await1;
        await await2;
        console.log("I will be logged after 500ms");
    }
    


    1. iShatokhin
      13.04.2017 19:16
      +3

      Если просто убрать await, то все равно будет неверно, т.к. console.log сработает сразу синхронно (можете проверить в консоле браузера). Правильный вариант опять же будет с промисами:


      async function inParallel() {
          console.time("I will be logged after");
          const await1 = pause500ms();
          const await2 = pause500ms();
          await Promise.all([await1, await2]);
          console.timeEnd("I will be logged after"); // => I will be logged after: 500.319ms
      }


      1. Kot_DaVinchi
        13.04.2017 19:33

        Проверил ещё раз. Исправленный мной код работает нормально. У нас разные консоли?


        1. Kanumowa
          13.04.2017 20:02

          Согласен Kot_DaVinchi, ваш код работает нормально, но если пойти дальше объясните плз почему время исполнения разное?

          async function inParallel() {
              const await1 = await pause500ms();
              const await2 = pause500ms();
              await await1;
              await await2;
              console.log("I will be logged after 1000ms");
          }

          async function inParallel() {
              const await1 = pause500ms();
              const await2 = await pause500ms();
              await await1;
              await await2;
              console.log("I will be logged after 500ms");
          }


          1. Ivanq
            13.04.2017 20:17
            +3

            async function inParallel() {
                const await1 = await pause500ms(); // Ждем 500 мс и записываем результат (undefined) в await1
                const await2 = pause500ms(); // Записываем промис, который тут же начинает выполняться
                await await1; // Ждем await1 (undefined) => пропускаем
                await await2; // Ждем await1 (pause500ms()) => Ждем еще 500 мс
                console.log("I will be logged after 1000ms");
            }


            1. Ivanq
              13.04.2017 21:07
              +1

              Соответственно,


              async function inParallel() {
                  const await1 = pause500ms(); // Записываем промис, который тут же начинает выполняться
                  const await2 = await pause500ms(); // Ждем 500 мс и записываем результат (undefined) в await1; к этому времени await1 уже также закончился.
                  await await1; // Ждем await1 (pause500ms()), который закончился => идем дальше
                  await await2; // Ждем await2 (undefined) => пропускаем
                  console.log("I will be logged after 500ms");
              }


          1. Kot_DaVinchi
            13.04.2017 20:37

            Вызов функции pause500ms инициирует setTimeout и возвращает promice в состоянии pending(в состоянии когда результата ещё нет). Директива await дожидается когда promice будет в состоянии resolve(или reject).
            И в итоге, в первом случае мы инициируем первый таймаут и дожидаемся когда он разрешится. После чего инициируем второй и в 5 строчке снова ждём. Во втором случае мы инициируем первый таймаут, инициируем второй таймаут и ждём пока второй разрешится, т.е. оба промайса разрешаются одновременно. Что важно, они не выполняют код одновременно(у нас однопоточная модель), они оба ждут какого-то внешнего события.


        1. iShatokhin
          13.04.2017 20:16
          +2

          Простите мою невнимательность, я думал, вы цитируете код из статьи, и убрал await совсем. Моя ошибка.


  1. justboris
    13.04.2017 22:32
    +4

    Кажется, в комментариях выше разбираюстся в тонкостях async/await лучше, чем в самой статье.


    Уважаемый переводчик, почему вы решили переводить именно эту не самую лучшую статью?


    1. Binjo
      14.04.2017 23:22
      +1

      Суть в том, чтобы у людей возник интерес. Люди начинают обсуждать, объяснять друг другу. Это же замечательно.


  1. IgorKlopov
    13.04.2017 23:46
    +1

    await await1;
    await await2;

    К сожалению, этот код будет работать не на 100% корректно. Представьте, что await1 резолвнется (resolve) через 1000мс, а await2 реджектнется (reject) через 500мс. Пока мы ждем await await1, мы НЕ ждем await await2. Таким образом, await2 отработает как unhandled rejection. И мы не сможем поймать ошибку (которая reject(error) внутри await2) в try-catch выше по стеку.

    Promise.all([promise1, promise2]) единственный способ когда нужно ждать несколько промисов одновременно. Так как Promise.all начинает ждать (await) все промисы сразу, то и ошибку можно поймать от любого ожидаемого промиса.


    1. Yozi
      14.04.2017 11:00

      async function badAsync(){
          await await1; // стартовал первый промис
          // первый промис выполняется, второй ещё нет, ждём
          // первый промис зарезолвился через 1000 мс
          await await2; // первый уже зарезолвился, стартовали второй
          // ждём второй
          // второй падает через 500 мс
      }
      
      async function test(){
           try{
               await badAsync();
           } catch(err){
                // поймали reject от await2 через 1500мс
           }
      }

      Итого через 1500 мс я поймал ошибку от второго промиса, и по-моему это корректно, так как я по каким-то причинам не мог выполнить второй запрос параллельно с первым (например ожидал его результата). Ничего не потерял, или Вы о чём-то другом?


      Для меня


      Пока мы ждем await await1, мы НЕ ждем await await2. Таким образом, await2 отработает как unhandled rejection

      Звучит как минимум странно, потому что мы ещё не дошли до вызова и выполнения await2 при последовательном вызове, и потому он не мог выполниться раньше. И уж тем более он ни как не мог стать unhandled rejection


      1. IgorKlopov
        14.04.2017 22:55

        Пожалуйста, добейтесь параллельной работы обоих промисов.
        Я говорю о таком коде:

        const await1 = new Promise((res) => setTimeout(res, 1000));
        const await2 = new Promise((_, rej) => setTimeout(rej, 500));
        await await1;
        await await2;


        1. Yozi
          14.04.2017 23:12

          Хорошо, 2 параллельных запроса, но с синхронным ожиданием их результата.
          Ваше утверждение про неуловимые исключения по-прежнему не соответствует коду


          await2 отработает как unhandled rejection. И мы не сможем поймать ошибку (которая reject(error) внутри await2) в try-catch выше по стеку.

          async function test(){
              const await1 = new Promise((res) => setTimeout(res, 1000));
              const await2 = new Promise((_, rej) => setTimeout(()=>rej(new Error("myError")), 500));
              console.log("1"); // я буду выведена в консоль сразу
              await await1;
              console.log("2");  // я только через 1000 мс
              await await2; // я выкину ошибку, которая была создана 500 мс назад
              console.log("3"); // меня не напечатают
          }
          
          async function test2(){
             try{
                await test();
             } catch(err){
                console.log("Catched!", err); // Ошибка из await2 
             }
          }

          Ещё можно было внутри test сделать try catch, тоже бы сработал. Я полностью согласен, что Promise.all тот самый правильный способ для параллельных запросов, но не мог не заметить ошибку про потерянные исключения. Хотя она встречается если забыть await


          1. IgorKlopov
            14.04.2017 23:21

            У вас разве в логе нет после единицы UnhandledPromiseRejectionWarning?


            1. Yozi
              14.04.2017 23:25

              Я не случайно в reject добавил ошибку к вашему коду, а потом залогировал то, что я поймал в test2.


          1. IgorKlopov
            14.04.2017 23:32

            Тогда поясню свою позицию про try-catch.
            Так как нас даже warning предупреждает (In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code) что unhandled rejection это фатально, то для эмуляции будущих версий (и для fail fast, без unpredicted state) я выполняю per-process что-то типа

            process.on('unhandledRejection', (error) => {
            console.error('>>> unhandledRejection pid', process.pid);
            console.error(error);
            process.exit(6);
            });

            Поэтому у автора два bad practice. Первый — пропускать unhandled rejection, а второй — использовать два await вместо Promise.all. Конечно, если пропускать unhandled rejection, то можно catch если есть await, но чаще всего await просто забыт.


            1. iShatokhin
              16.04.2017 14:24

              использовать два await вместо Promise.all

              Кстати, да, удивляет, что в стандарте нет простого и лаконичного решения:


              const arr = await [await1, await2];


              1. IgorKlopov
                16.04.2017 14:56

                const arr = await Promise.all([ await1, await2 ]);

                именно это и делает. Promise.all создает общий промис, а await ждет его.


                1. iShatokhin
                  16.04.2017 14:57

                  Это понятно, я имел ввиду короткую запись.


                  1. IgorKlopov
                    16.04.2017 15:03
                    +1

                    Короткая запись тоже невозможна. По стандарту await возвращает значение если оно не является промисом. await [ 15, 17 ] должен вернуть массив [ 15, 17 ], не заглядывая внутрь него. Заглядывать внутрь массива — значит тратить время на то, что программисту не нужно. Если программисту нужно явно ждать все промисы массива, на это есть Promise.all.


                1. iShatokhin
                  16.04.2017 15:04

                  Попробовал в консоли такой код, и он работает.


                  const arr = [await await1, await await2];

                  Как мне показалось, работает эквивалентно await Promise.all. Поправьте, если я не прав.


                  1. IgorKlopov
                    16.04.2017 15:06
                    +1

                    Работает, но не параллельно, а последовательно. И я боюсь, что, как и у автора, unhandled rejection. Полностью как у автора, только в массиве.


                    1. iShatokhin
                      17.04.2017 09:23

                      Промисы были инициализированы выше по коду (сейчас в состоянии pending), т.ч. получается параллельно (если такое слово вообще применимо к JS), и время ожидания равно ожиданию дольшего из них, как и в случае с Promise.all. Специально тестировал с разными таймаутами.


                      1. Maiami
                        17.04.2017 23:19
                        +1

                        Промисы были инициализированы выше по коду (сейчас в состоянии pending), т.ч. получается параллельно

                        Природа event-loop не даст войти внутрь промиса пока не появится в коде «свободное окно» вроде then/await или отсутствие других вычислений, пока они именно что в ожидании на выполнение

                        async function delay(time) {
                            return new Promise(resolve => setTimeout(resolve, time))
                        }
                        
                        async function main() {
                            try {
                                console.time('await array')
                                let arr = [await delay(500), await delay(1000)]        
                                console.timeEnd('await array')
                            
                                console.time('Promise.all')
                                arr = await Promise.all([delay(500), delay(1000)])
                                console.timeEnd('Promise.all')
                            }
                            catch(err) {
                                console.log(err)
                            }
                        }
                        
                        main()
                        

                        >node index.js
                        await array: 1501.689ms
                        Promise.all: 1000.757ms
                        


                        1. Maiami
                          17.04.2017 23:39
                          +1

                          Хотя нет, если написать именно как iShatokhin предлагает:

                          const await1 = delay(500)
                          const await2 = delay(500)
                          let arr = [await await1, await await2]
                          

                          await array: 501.792ms
                          Promise.all: 500.836ms
                          

                          То всё работает, но для второго await остается Unhandled promise rejection


                          1. iShatokhin
                            18.04.2017 00:27
                            +1

                            Насколько я помню стандарт, выполнение Promise помещается в начало стека event-loop (так называемые microtasks), никакого "окна" ждать не надо, но тогда в случае двух таких промиссов try-catch бесполезен, т.к. вы верно заметили — будет unhandledRejection.


                            Уже понял, что сахар искать бесполезно, остается только старый-добрый Promise.all Спасибо за разъяснения.


  1. barkalov
    14.04.2017 05:06
    +3

    Одним из основных преимуществ JavaScript является то, что всё асинхронно
    Очень смелое заявление. Так уж всё? Ну, кроме http-запросов и таймаутов?

    node.js != js


  1. Gennadii_M
    14.04.2017 07:54
    +2

    Висят 2 прищепки, особенно левая =>
    В JS асинхронно всё, особенно console.log(); )
    Автору было бы не плохо перед async/await посмотреть JSConf EU 2014 про call stack и callback queue.