Сегодня мы попытаемся построить и запустить магазин мороженого, при этом одновременно изучить асинхронный JavaScript. Также вы узнаете, как использовать:
- Callbacks.
- Promises.
- Async / Await.
Вот что мы рассмотрим в этой статье:
- Что такое асинхронный JavaScript.
- Синхронный и асинхронный JavaScript.
- Как работают Callbacks в JavaScript.
- Как работают Promises в JavaScript.
- Как работает Async / Await в JavaScript.
Это — перевод оригинальной статьи на freeCodeCamp от автора Joy Shaheb.
Что такое асинхронный JavaScript
Если вы хотите разрабатывать эффективные проекты, то эта концепция идеально подходит вам.
Теория асинхронного JavaScript помогает разбивать большие сложные проекты на более мелкие задачи.
Вы можете использовать любую из этих трех техник — Callbacks, Promises или Async/await — для выполнения небольших задач таким образом, чтобы получить наилучшие результаты.
Синхронный и асинхронный JavaScript
Что такое синхронная система
В синхронной системе задачи выполняются одна за другой.
Представьте, что у вас всего одна рука для выполнения 10 задач. Из-за этого вы должны выполнять по одной задаче за один раз. По умолчанию JavaScript является синхронным или однопоточным. Подумайте об этом так: один поток означает одну руку, которой можно что-то делать.
Что такое асинхронная система
В этой системе задачи выполняются независимо друг от друга.
Представьте, что для выполнения 10 задач у вас есть 10 рук. Таким образом, каждая рука может выполнять задачу независимо друг от друга и одновременно.
Итак, в чём разница между синхронным и асинхронным JS
В синхронной системе изображения находятся в одном потоке обработки. Одно изображение не может обогнать другое. Загрузка завершается по очереди. Если первое изображение заканчивает загрузку, останавливается и следующее изображение.
В асинхронной системе изображения находятся на разных дорожках. Они закончат загрузку в своем собственном темпе. Никто ни перед кем не останавливается, если вдруг возникнет ошибка.
Примеры синхронного и асинхронного кода
Прежде чем начать наш проект, давайте рассмотрим несколько примеров.
Пример синхронного кода
Чтобы протестировать синхронную систему, напишите этот код на JavaScript:
console.log(" I ");
console.log(" eat ");
console.log(" Ice Cream ");
Вот результат в консоли:
Пример асинхронного кода
Допустим, чтобы съесть мороженое, требуется две секунды. Теперь давайте протестируем асинхронную систему. Напишите приведенный ниже код на JavaScript.
console.log("I");
// This will be shown after 2 seconds
setTimeout(()=>{
console.log("eat");
}, 2000);
console.log("Ice Cream");
Примечание: Не волнуйтесь, функцию setTimeout()
мы обсудим позже.
И вот результат в консоли:
Теперь, когда вы знаете разницу между синхронными и асинхронными операциями, давайте создадим наш магазин мороженого.
Как настроить наш проект
Для этого проекта вы можете просто открыть Codepen.io и начать кодить. Или вы можете сделать это в VS code или другом редакторе по вашему выбору.
Откройте раздел JavaScript, а затем откройте консоль разработчика. Мы напишем наш код и посмотрим результаты в консоли.
Что такое обратные вызовы в JavaScript
Когда вы вкладываете функцию в другую функцию в качестве аргумента, это называется обратным вызовом или callback.
Вот иллюстрация обратного вызова:
Не волнуйтесь, через минуту мы увидим несколько примеров обратных вызовов.
Зачем использовать обратные вызовы
При выполнении сложной задачи мы разбиваем ее на более мелкие шаги. Чтобы установить связь между этими шагами по времени (необязательно) и порядку, мы используем обратные вызовы.
Взгляните на этот пример:
Вот те небольшие шаги, которые необходимо сделать, чтобы приготовить мороженое.
Также обратите внимание, что в этом примере порядок действий и время имеют решающее значение. Вы не можете просто нарезать фрукты и подать мороженое.
В то же время, если предыдущий шаг не завершен, мы не можем перейти к следующему.
Магазин будет состоять из двух частей:
- В кладовой будут храниться все ингредиенты [Бэкенд].
- Производить мороженое мы будем на нашей кухне [Фронтенд].
Создаём и сохраняем данные
Теперь давайте создадим наши ингредиенты, то есть фрукты, внутри объекта.
let stocks = {
Fruits : ["strawberry", "grapes", "banana", "apple"]
}
Дополним наши ингредиенты вафельными стаканчиками, топпингами и прочим:
let stocks = {
Fruits : ["strawberry", "grapes", "banana", "apple"],
liquid : ["water", "ice"],
holder : ["cone", "cup", "stick"],
toppings : ["chocolate", "peanuts"],
};
Весь бизнес зависит от того, что заказывает клиент. Как только мы получаем заказ, мы начинаем производство, а затем подаем мороженое. Итак, мы создадим две функции:
- order.
- production.
Вот как все это работает:
Получаем заказ, выбираем ингредиенты, готовим заказ и отдаём его.
Давайте создадим наши функции. Здесь мы будем использовать стрелочные функции:
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
.
Вот результат в нашей консоли:
Сохраните этот код и удалите все, но не удаляйте нашу переменную stocks.
В первой функции передайте еще один аргумент, чтобы мы могли получить заказ с названием нужного фрукта:
// Function 1
let order = (fruit_name, call_production) => {
call_production();
};
// Function 2
let production = () => {};
// Trigger ????
order("", production);
Обозначим последовательность шагов и нужное время для их выполнения.
На этой схеме видно, что шаг 1 — размещение заказа, который занимает 2 секунды. Затем шаг 2 — нарезать фрукты (2 секунды), шаг 3 — добавить воду и лед (1 секунда), шаг 4 — запустить машину (1 секунда), шаг 5 — выбрать контейнер (2 секунды), шаг 6 — выбрать начинку (3 секунды) и шаг 7 — подать мороженое, что занимает 2 секунды.
Для определения нужного времени отлично подходит функция setTimeout()
, так как она использует callback
, принимая функцию в качестве аргумента.
Теперь давайте выберем наши фрукты и воспользуемся этой функцией:
// 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 секунды.
Если вам интересно, как мы выбрали именно клубнику, вот код с форматом:
Ничего не удаляйте. Теперь мы начнем писать функцию для производства мороженого. Мы опять будем использовать стрелочные функции:
let production = () => {
setTimeout(() => {
console.log("production has started")
},0000)
};
Результат:
Добавим еще одну функцию в setTimeout, чтобы нарезать фрукты.
let production = () => {
setTimeout(()=> {
console.log("production has started")
setTimeout(()=> {
console.log("The fruit has been chopped")
},2000)
},0000)
};
Результат:
Давайте завершим полный процесс производства мороженого, вложив новые функции внутрь существующих функций — это и есть обратный вызов, помните?
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)
};
Смущены тем, как всё выглядит? Это ад обратных вызовов. 🙂 Они всегда выглядят примерно вот так:
Как же сделать так, чтобы код выглядел красивым?
Как использовать Promises, чтобы избежать ада обратного вызова
Promises были изобретены для избавления от ада обратных вызовов и для лучшей обработки наших задач.
Вот, как они выглядят:
Разберём, как они работают.
Как показано на графиках выше, promise имеет три состояния:
- Ожидание. Это начальная стадия. Здесь ничего не происходит. Подумайте об этом так: ваш клиент не торопится делать заказ. Он ещё ничего не заказал.
- Решено. Это означает, что ваш клиент получил свою еду и доволен.
- Отклонено. Это означает, что ваш клиент не получил свой заказ и покинул ресторан.
Давайте применим обещания в нашем примере с производством мороженого.
Сначала нам нужно понять еще четыре вещи:
- Взаимосвязь между временем и работой.
- Как работает цепочка Promise.
- Как работает обработка ошибок.
- Как работает обработчик .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 частей:
- Решено [мороженое доставлено].
- Отклонено [клиент не получил мороженое].
То есть 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
Результат после двух секунд ожидания:
Цепочки обещаний
В этом методе мы определяем, что нам нужно сделать, когда первая задача будет выполнена, используя обработчик .then
.
Обработчик .then
возвращает promise, когда наше первое обещание будет разрешено.
Такой механизм похож на то, как вы даете кому-то инструкции. Вы говорите кому-то: «Сначала сделай это, потом сделай то, потом другое, потом…, потом…, потом…», и так далее.
- Первая задача — это первоначальный Promise.
- Остальные задачи возвращают наше обещание после того, как будет выполнена одна небольшая часть работы.
Давайте реализуем это в нашем проекте.
Примечание: не забудьте написать слово return
внутри обработчика .then
. В противном случае он не будет работать должным образом. Если вам интересно, зачем он нужен, попробуйте удалить return
, когда мы закончим все шаги:
order(2000, () => console.log(`${stocks.Fruits[0]} was selected`));
.then(() => {
return order(0000, () => console.log('production has started'));
})
Вот результат:
Используя ту же систему, закончим наш проект.
// 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"))
})
Результат:
Обработка ошибок
Нам нужен способ обработки ошибок, когда что-то идет не так. Чтобы отловить ошибки, давайте изменим нашу переменную на false
.
let is_shop_open = false;
Это означает, что наш магазин закрыт. Мы больше не продаем мороженое нашим клиентам.
Чтобы справиться с ошибкой, мы используем обработчик .catch
. Как и .then
, он также возвращает Promise, но только в том случае, если наше первоначальное обещание отклонено.
Итак, напоминаем:
- .then работает, когда Promise сработал как следует.
- .catch работает, когда Promise отвергнут.
Таким образом, между предыдущим обработчиком .then и обработчиком .catch не должно быть вообще ничего.
.catch(() => {
console.log("Customer left")
})
Результат:
Что значит вывод после исполнения кода:
- Первое сообщение приходит от части reject() нашего Promise.
- Второе сообщение приходит из обработчика .catch.
Как использовать обработчик .finally()
Существует обработчик finally, который работает независимо от того, был ли наш Promise выполнен или отклонен.
Например: независимо от того, обслужили ли мы хотя бы одного клиента или 100 клиентов, наш магазин закроется в конце дня.
Вот пример такого кода:
.finally(() => {
console.log("end of day")
})
Результат:
Как работает 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 выполнится и вернет результат.
Вернемся к нашему магазину мороженого. Мы не знаем, какой топпинг предпочтет покупатель: шоколад или арахис. Нам нужно остановить машину и спросить клиента, чего бы он хотел.
Заметьте, что мы остановили только кухню, но персонал вне кухни продолжает делать такие вещи, как:
- мытье посуды;
- уборка столов;
- приём заказов, и т.д.
Давайте создадим 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")
Вот результат:
Мы буквально выходим из кухни, чтобы спросить клиента: «Какой топпинг вы предпочитаете?». В это время другие задачи не заканчиваются.
Как только мы узнаем о выборе топпинга, мы возвращаемся на кухню и заканчиваем заказ.
При использовании Async/Await вы также можете использовать обработчики .then
, .catch
и .finally
, которые являются основной частью Promises.
Запускаем магазин мороженого
Для этого создадим две функции:
- kitchen для приготовления мороженого;
- 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();
Вот, что вы должны увидеть, если магазин считается открытым:
А вот, что будет, если магазин закрыт.
Завершим наш проект. Для этого вспомним, какие задачи должны выполняться в магазине:
Открываем магазин:
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.
Благодарим за то, что дочитали до конца!