Написать пост

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises

Аватарка пользователя sudo >: )
для
Логотип компании Tproger
Tproger

Рассказываем, как построить приложение с Async/Await на JavaScript. Также объясняем, как работают Callbacks и Promises.

Сегодня мы попытаемся построить и запустить магазин мороженого, при этом одновременно изучить асинхронный JavaScript. Также вы узнаете, как использовать:

  1. Callbacks.
  2. Promises.
  3. Async / Await.

Вот что мы рассмотрим в этой статье:

  1. Что такое асинхронный JavaScript.
  2. Синхронный и асинхронный JavaScript.
  3. Как работают Callbacks в JavaScript.
  4. Как работают Promises в JavaScript.
  5. Как работает Async / Await в JavaScript.

Это — перевод оригинальной статьи на freeCodeCamp от автора Joy Shaheb.

Ещё у нас есть видеоверсия урока от автора (осторожно, английский язык):

Превью видео n5ZtTO1ArWg

Итак, давайте начнём!

Что такое асинхронный JavaScript

Если вы хотите разрабатывать эффективные проекты, то эта концепция идеально подходит вам.

Теория асинхронного JavaScript помогает разбивать большие сложные проекты на более мелкие задачи.

Вы можете использовать любую из этих трех техник – Callbacks, Promises или Async/await – для выполнения небольших задач таким образом, чтобы получить наилучшие результаты.

Синхронный и асинхронный JavaScript

Что такое синхронная система

В синхронной системе задачи выполняются одна за другой.

Представьте, что у вас всего одна рука для выполнения 10 задач. Из-за этого вы должны выполнять по одной задаче за один раз.

Посмотрите на GIF, здесь происходит одно действие за раз:

Вы увидите, что пока первое изображение не загрузится полностью, второе изображение не начнет загружаться.

По умолчанию JavaScript является синхронным или однопоточным. Подумайте об этом так: один поток означает одну руку, которой можно что-то делать.

Что такое асинхронная система

В этой системе задачи выполняются независимо друг от друга.

Представьте, что для выполнения 10 задач у вас есть 10 рук. Таким образом, каждая рука может выполнять задачу независимо друг от друга и одновременно.

Посмотрите на GIF. Вы увидите, что каждое изображение загружается одновременно.

Все изображения загружаются в своем собственном темпе. Ни одно из них не ждёт загрузки других.

Итак, в чём разница между синхронным и асинхронным JS

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

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 1

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

Примеры синхронного и асинхронного кода

Прежде чем начать наш проект, давайте рассмотрим несколько примеров.

Пример синхронного кода

Чтобы протестировать синхронную систему, напишите этот код на JavaScript:

			console.log(" I ");

console.log(" eat ");

console.log(" Ice Cream ");
		

Вот результат в консоли:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 2

Пример асинхронного кода

Допустим, чтобы съесть мороженое, требуется две секунды. Теперь давайте протестируем асинхронную систему. Напишите приведенный ниже код на JavaScript.

			console.log("I");

// This will be shown after 2 seconds

setTimeout(()=>{
  console.log("eat");
},2000)

console.log("Ice Cream")
		

Примечание: Не волнуйтесь, функцию setTimeout() мы обсудим позже.

И вот результат в консоли:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 3

Теперь, когда вы знаете разницу между синхронными и асинхронными операциями, давайте создадим наш магазин мороженого.

Как настроить наш проект

Для этого проекта вы можете просто открыть Codepen.io и начать кодить. Или вы можете сделать это в VS code или другом редакторе по вашему выбору.

Откройте раздел JavaScript, а затем откройте консоль разработчика. Мы напишем наш код и посмотрим результаты в консоли.

Что такое обратные вызовы в JavaScript

Когда вы вкладываете функцию в другую функцию в качестве аргумента, это называется обратным вызовом или callback.

Вот иллюстрация обратного вызова:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 4

Не волнуйтесь, через минуту мы увидим несколько примеров обратных вызовов.

Зачем использовать обратные вызовы

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

Взгляните на этот пример:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 5

Вот те небольшие шаги, которые необходимо сделать, чтобы приготовить мороженое.

Также обратите внимание, что в этом примере порядок действий и время имеют решающее значение. Вы не можете просто нарезать фрукты и подать мороженое.

В то же время, если предыдущий шаг не завершен, мы не можем перейти к следующему.

Магазин будет состоять из двух частей:

  1. В кладовой будут храниться все ингредиенты [Бэкенд].
  2. Производить мороженое мы будем на нашей кухне [Фронтенд].

Создаём и сохраняем данные

Теперь давайте создадим наши ингредиенты, то есть фрукты, внутри объекта. 

			let stocks = {
    Fruits : ["strawberry", "grapes", "banana", "apple"]
 }
		

Дополним наши ингредиенты вафельными стаканчиками, топпингами и прочим:

			let stocks = {
    Fruits : ["strawberry", "grapes", "banana", "apple"],
    liquid : ["water", "ice"],
    holder : ["cone", "cup", "stick"],
    toppings : ["chocolate", "peanuts"],
 };
		

Весь бизнес зависит от того, что заказывает клиент. Как только мы получаем заказ, мы начинаем производство, а затем подаем мороженое. Итак, мы создадим две функции:

  1. order;
  2. production.

Вот как все это работает:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 6
Получаем заказ, выбираем ингредиенты, готовим заказ и отдаём его. 

Давайте создадим наши функции. Здесь мы будем использовать стрелочные функции:

			let order = () =>{};

let production = () =>{};
		

Теперь давайте установим связь между этими двумя функциями с помощью обратного вызова, например, так:

			let order = (call_production) =>{

  call_production();
};

let production = () =>{};
		

Давайте проведем небольшой тест

Мы будем использовать функцию console.log() для проведения тестов, чтобы развеять все сомнения, которые могут возникнуть относительно того, как мы установили связь между двумя функциями.

			let order = (call_production) =>{

console.log("Order placed. Please call production")

// function ???? is being called 
  call_production();
};

let production = () =>{

console.log("Production has started")

};
		

Чтобы запустить тест, мы вызовем функцию order. И добавим в качестве аргумента вторую функцию с именем production.

Вот результат в нашей консоли:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 7

Сохраните этот код и удалите все, но не удаляйте нашу переменную stocks.

В первой функции передайте еще один аргумент, чтобы мы могли получить заказ с названием нужного фрукта:

			// Function 1

let order = (fruit_name, call_production) =>{

  call_production();
};

// Function 2

let production = () =>{};


// Trigger ????

order("", production);
		

Обозначим последовательность шагов и нужное время для их выполнения.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 8

На этой схеме видно, что шаг 1 – размещение заказа, который занимает 2 секунды. Затем шаг 2 – нарезать фрукты (2 секунды), шаг 3 – добавить воду и лед (1 секунда), шаг 4 – запустить машину (1 секунда), шаг 5 – выбрать контейнер (2 секунды), шаг 6 – выбрать начинку (3 секунды) и шаг 7 – подать мороженое, что занимает 2 секунды.

Для определения нужного времени отлично подходит функция setTimeout(), так как она использует callback, принимая функцию в качестве аргумента.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 9

Теперь давайте выберем наши фрукты и воспользуемся этой функцией:

			// 1st Function

let order = (fruit_name, call_production) =>{

  setTimeout(function(){

    console.log(`${stocks.Fruits[fruit_name]} was selected`)

// Order placed. Call production to start
   call_production();
  },2000)
};

// 2nd Function

let production = () =>{
  // blank for now
};

// Trigger ????
order(0, production);
		

Вот результат в консоли. Обратите внимание, что результат отображается через 2 секунды.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 10

Если вам интересно, как мы выбрали именно клубнику, вот код с форматом:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 11

Ничего не удаляйте. Теперь мы начнем писать функцию для производства мороженого. Мы опять будем использовать стрелочные функции:

			let production = () =>{

  setTimeout(()=>{
    console.log("production has started")
  },0000)

};
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 12

Добавим еще одну функцию в setTimeout, чтобы нарезать фрукты. 

			let production = () =>{
  
  setTimeout(()=>{
    console.log("production has started")


    setTimeout(()=>{
      console.log("The fruit has been chopped")
    },2000)


  },0000)
};
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 13

Давайте завершим полный процесс производства мороженого, вложив новые функции внутрь существующих функций – это и есть обратный вызов, помните?

			let production = () =>{

  setTimeout(()=>{
    console.log("production has started")
    setTimeout(()=>{
      console.log("The fruit has been chopped")
      setTimeout(()=>{
        console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} Added`)
        setTimeout(()=>{
          console.log("start the machine")
          setTimeout(()=>{
            console.log(`Ice cream placed on ${stocks.holder[1]}`)
            setTimeout(()=>{
              console.log(`${stocks.toppings[0]} as toppings`)
              setTimeout(()=>{
                console.log("serve Ice cream")
              },2000)
            },3000)
          },2000)
        },1000)
      },1000)
    },2000)
  },0000)

};
		
Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 14

Смущены тем, как всё выглядит? Это ад обратных вызовов. ? Они всегда выглядят примерно вот так:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 15

Как же сделать так, чтобы код выглядел красивым?

Как использовать Promises, чтобы избежать ада обратного вызова

Promises были изобретены для избавления от ада обратных вызовов и для лучшей обработки наших задач.

Вот, как они выглядят:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 16

Разберём, как они работают.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 17

Как показано на графиках выше, promise имеет три состояния:

  1. Ожидание. Это начальная стадия. Здесь ничего не происходит. Подумайте об этом так: ваш клиент не торопится делать заказ. Он ещё ничего не заказал.
  2. Решено. Это означает, что ваш клиент получил свою еду и доволен.
  3. Отклонено. Это означает, что ваш клиент не получил свой заказ и покинул ресторан.

Давайте применим обещания в нашем примере с производством мороженого.

Сначала нам нужно понять еще четыре вещи:

  1. Взаимосвязь между временем и работой.
  2. Как работает цепочка Promise.
  3. Как работает обработка ошибок.
  4. Как работает обработчик .finally

Разберем каждую из этих концепций по очереди, делая небольшие шаги.

Взаимосвязь между временем и работой

Если вы помните, это наши шаги и время, которое занимает каждый шаг, чтобы сделать мороженое.

Чтобы это произошло, давайте создадим переменную в JavaScript:

			let is_shop_open = true;
		

Теперь создайте функцию с именем order и передайте ей два аргумента с именами time, work:

			let order = ( time, work ) =>{

  }
		

Теперь мы дадим обещание (Promise) нашему клиенту: “Мы подадим вам мороженое”:

			let order = ( time, work ) =>{

  return new Promise( ( resolve, reject )=>{ } )

  }
		

Наше обещание состоит из 2 частей:

  1. Решено [мороженое доставлено].
  2. Отклонено [клиент не получил мороженое].

То есть Promise состоит из Resolve и Reject.

			let order = ( time, work ) => {

  return new Promise( ( resolve, reject )=>{

    if( is_shop_open ){

      resolve( )

    }

    else{

      reject( console.log("Our shop is closed") )

    }

  })
}
		

Давайте добавим коэффициенты времени и работы внутри нашего promise с помощью функции setTimeout() внутри оператора if.

Примечание: В реальной жизни вы можете обойтись и без фактора времени. Это зависит от требований проекта.

			let order = ( time, work ) => {

  return new Promise( ( resolve, reject )=>{

    if( is_shop_open ){

      setTimeout(()=>{

       // work is ???? getting done here
        resolve( work() )

// Setting ???? time here for 1 work
       }, time)

    }

    else{
      reject( console.log("Our shop is closed") )
    }

  })
}
		

Теперь мы используем нашу только что созданную функцию для запуска производства.

			// Set ???? time here
order( 2000, ()=>console.log(`${stocks.Fruits[0]} was selected`))
//    pass a ☝️ function here to start working
		

Результат после двух секунд ожидания:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 18

Цепочки обещаний

В этом методе мы определяем, что нам нужно сделать, когда первая задача будет выполнена, используя обработчик .then.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 19

Обработчик .then возвращает promise, когда наше первое обещание будет разрешено.

Такой механизм похож на то, как вы даете кому-то инструкции. Вы говорите кому-то: “Сначала сделай это, потом сделай то, потом другое, потом…, потом…, потом…”, и так далее.

  1. Первая задача – это первоначальный Promise.
  2. Остальные задачи возвращают наше обещание после того, как будет выполнена одна небольшая часть работы.

Давайте реализуем это в нашем проекте.

Примечание: не забудьте написать слово return внутри обработчика .then. В противном случае он не будет работать должным образом. Если вам интересно, зачем он нужен, попробуйте удалить return, когда мы закончим все шаги:

			order(2000,()=>console.log(`${stocks.Fruits[0]} was selected`))

.then(()=>{
  return order(0000,()=>console.log('production has started'))
})
		

Вот результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 20

Используя ту же систему, закончим наш проект.

			// step 1
order(2000,()=>console.log(`${stocks.Fruits[0]} was selected`))

// step 2
.then(()=>{
  return order(0000,()=>console.log('production has started'))
})

// step 3
.then(()=>{
  return order(2000, ()=>console.log("Fruit has been chopped"))
})

// step 4
.then(()=>{
  return order(1000, ()=>console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`))
})

// step 5
.then(()=>{
  return order(1000, ()=>console.log("start the machine"))
})

// step 6
.then(()=>{
  return order(2000, ()=>console.log(`ice cream placed on ${stocks.holder[1]}`))
})

// step 7
.then(()=>{
  return order(3000, ()=>console.log(`${stocks.toppings[0]} as toppings`))
})

// Step 8
.then(()=>{
  return order(2000, ()=>console.log("Serve Ice Cream"))
})
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 21

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

Нам нужен способ обработки ошибок, когда что-то идет не так. Чтобы отловить ошибки, давайте изменим нашу переменную на false.

			let is_shop_open = false;
		

Это означает, что наш магазин закрыт. Мы больше не продаем мороженое нашим клиентам.

Чтобы справиться с ошибкой, мы используем обработчик .catch. Как и .then, он также возвращает Promise, но только в том случае, если наше первоначальное обещание отклонено.

Итак, напоминаем:

  1. .then работает, когда Promise сработал как следует.
  2. .catch работает, когда Promise отвергнут.

Таким образом, между предыдущим обработчиком .then и обработчиком .catch не должно быть вообще ничего.

			.catch(()=>{
  console.log("Customer left")
})
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 22

Что значит вывод после исполнения кода:

  1. Первое сообщение приходит от части reject() нашего Promise.
  2. Второе сообщение приходит из обработчика .catch.

Как использовать обработчик .finally()

Существует обработчик finally, который работает независимо от того, был ли наш Promise выполнен или отклонен.

Например: независимо от того, обслужили ли мы хотя бы одного клиента или 100 клиентов, наш магазин закроется в конце дня.

Вот пример такого кода:

			.finally(()=>{
  console.log("end of day")
})
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 23

Как работает Async / Await в JavaScript

Предполагается, что это лучший способ написания Promise, который помогает нам сохранять код простым и чистым.

Все, что вам нужно сделать, это написать слово async перед любой обычной функцией, и она станет обещанием.

Promises против Async/Await на JavaScript

До появления async/await для выполнения обещания мы писали следующее:

			function order(){
   return new Promise( (resolve, reject) =>{

    // Write code here
   } )
}
		

Теперь используем async:

			//???? the magical keyword
 async function order() {
    // Write code here
 }
		

Как использовать ключевые слова Try и Catch

Мы используем try для выполнения кода, а catch – для отлова ошибок. Это та же концепция, которую мы видели при рассмотрении обещаний.

Давайте сравним, как это работает.

В обещаниях мы использовали resolve и reject:

			function kitchen(){

  return new Promise ((resolve, reject)=>{
    if(true){
       resolve("promise is fulfilled")
    }

    else{
        reject("error caught here")
    }
  })
}

kitchen()  // run the code
.then()    // next step
.then()    // next step
.catch()   // error caught here
.finally() // end of the promise [optional]
		

Когда мы используем async/await, мы используем этот формат:

			//???? Magical keyword
async function kitchen(){

   try{
// Let's create a fake problem      
      await abc;
   }

   catch(error){
      console.log("abc does not exist", error)
   }

   finally{
      console.log("Runs code anyways")
   }
}

kitchen()  // run the code
		

Теперь, надеемся, вы понимаете разницу между Promises и Async/Await.

Как использовать Await в JavaScript

Ключевое слово await заставляет JavaScript ждать, пока Promise выполнится и вернет результат.

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

Заметьте, что мы остановили только кухню, но персонал вне кухни продолжает делать такие вещи, как:

  1. мытье посуды;
  2. уборка столов;
  3. приём заказов, и т.д.

Давайте создадим Promise, чтобы спросить, какой топпинг использовать. Процесс занимает три секунды.

			function toppings_choice (){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{

      resolve( console.log("which topping would you love?") )

    },3000)
  })
}
		

Теперь давайте сначала создадим функцию для кухни с async.

			async function kitchen(){

  console.log("A")
  console.log("B")
  console.log("C")
  
  await toppings_choice()
  
  console.log("D")
  console.log("E")

}

// Trigger the function

kitchen();
		

Давайте добавим другие задачи ниже kitchen().

			console.log("doing the dishes")
console.log("cleaning the tables")
console.log("taking orders")
		

Вот результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 24

Мы буквально выходим из кухни, чтобы спросить клиента: “Какой топпинг вы предпочитаете?”. В это время другие задачи не заканчиваются.

Как только мы узнаем о выборе топпинга, мы возвращаемся на кухню и заканчиваем заказ.

При использовании Async/Await вы также можете использовать обработчики .then, .catch и .finally, которые являются основной частью Promises.

Запускаем магазин мороженого

Для этого создадим две функции:

  1. kitchen для приготовления мороженого;
  2. time для назначения количества времени, которое займет каждая небольшая задача.

Сначала создадим функцию time:

			let is_shop_open = true;

function time(ms) {

   return new Promise( (resolve, reject) => {

      if(is_shop_open){
         setTimeout(resolve,ms);
      }

      else{
         reject(console.log("Shop is closed"))
      }
    });
}
		

Теперь создадим кухню:

			async function kitchen(){
   try{

     // instruction here
   }

   catch(error){
    // error management here
   }
}

// Trigger
kitchen();
		

Проверим, работает ли наша кухня:

			async function kitchen(){
   try{

// time taken to perform this 1 task
     await time(2000)
     console.log(`${stocks.Fruits[0]} was selected`)
   }

   catch(error){
     console.log("Customer left", error)
   }

   finally{
      console.log("Day ended, shop closed")
    }
}

// Trigger
kitchen();
		

Вот, что вы должны увидеть, если магазин считается открытым:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 25

А вот, что будет, если магазин закрыт.

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 26

Завершим наш проект. Для этого вспомним, какие задачи должны выполняться в магазине:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 27

Открываем магазин:

			let is_shop_open = true;
		

Добавляем все функции для нашей кухни:

			async function kitchen(){
    try{
	await time(2000)
	console.log(`${stocks.Fruits[0]} was selected`)

	await time(0000)
	console.log("production has started")

	await time(2000)
	console.log("fruit has been chopped")

	await time(1000)
	console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`)

	await time(1000)
	console.log("start the machine")

	await time(2000)
	console.log(`ice cream placed on ${stocks.holder[1]}`)

	await time(3000)
	console.log(`${stocks.toppings[0]} as toppings`)

	await time(2000)
	console.log("Serve Ice Cream")
    }

    catch(error){
	 console.log("customer left")
    }
}
		

Результат:

Асинхронный JavaScript: изучаем Async/Await, Callbacks и Promises 28

Заключение

Поздравляем, вы дочитали до конца! В этой статье вы изучили:

  1. Разницу между синхронными и асинхронными системами.
  2. Механизмы асинхронного JavaScript с использованием обратных вызовов, обещаний и Async/Await.

Благодарим за то, что дочитали до конца!

Следите за новыми постами
Следите за новыми постами по любимым темам
10К открытий10К показов