Async Await JavaScript Tutorial - Як чекати, поки функція закінчиться в JS

Коли закінчується асинхронна функція? І чому на це таке важке запитання?

Ну виявляється, що розуміння асинхронних функцій вимагає великих знань про те, як принципово працює JavaScript.

Давайте вивчимо цю концепцію та дізнаємось багато нового про JavaScript у процесі.

Ви готові? Ходімо.

Що таке асинхронний код?

За задумом JavaScript є синхронною мовою програмування. Це означає, що при виконанні коду JavaScript запускається у верхній частині файлу і проходить через код рядок за рядком, поки це не буде зроблено.

Результатом цього дизайнерського рішення є те, що одночасно може статися лише одне.

Ви можете думати про це так, ніби ви жонглюєте шістьма маленькими кульками. Поки ти жонглюєш, твої руки зайняті і нічого іншого не справляються.

Те ж саме і з JavaScript: після запуску коду він має повні руки з цим кодом. Ми називаємо це таким видом синхронного блокування коду . Тому що це фактично блокує запуск іншого коду.

Давайте повернемося до прикладу жонглювання. Що станеться, якщо ви захочете додати ще один кульку? Замість шести кульок ви хотіли жонглювати сімома кулями. Це може бути проблемою.

Ви не хочете припиняти жонглювання, бо це просто так весело. Але ви також не можете піти і отримати ще один м’яч, тому що це означало б, що вам доведеться зупинитися.

Рішення? Доручіть роботу другові або члену сім'ї. Вони не жонглюють, тому можуть піти і отримати м’яч за вас, а потім кинути його у ваше жонглювання в той момент, коли ваша рука вільна, і ви готові додати ще один м’яч в ​​середині жонглювання.

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

Хто виконує іншу роботу?

Гаразд, отже, ми знаємо, що JavaScript синхронний і ледачий. Вона не хоче виконувати всю роботу сама, тому переводить її на щось інше.

Але хто ця загадкова сутність, яка працює на JavaScript? І як його наймають для роботи на JavaScript?

Ну, давайте розглянемо приклад асинхронного коду.

const logName = () => { console.log("Han") } setTimeout(logName, 0) console.log("Hi there")

Запуск цього коду призводить до наступного виводу в консолі:

// in console Hi there Han

Гаразд Що відбувається?

Виявляється, спосіб, яким ми виробляємо роботу в JavaScript, полягає у використанні специфічних для середовища функцій та API. І це викликає велику плутанину в JavaScript.

JavaScript завжди працює в середовищі.

Часто це середовище є браузером. Але це може бути і на сервері з NodeJS. Але в чому різниця?

Різниця - і це важливо - полягає в тому, що браузер і сервер (NodeJS), з точки зору функціональності, не є рівнозначними. Вони часто схожі, але не однакові.

Проілюструємо це на прикладі. Скажімо, JavaScript є головним героєм епічної книги про фентезі. Просто звичайна фермерська дитина.

А тепер припустимо, що цей фермерський хлопець знайшов два костюми спеціальної броні, які надавали їм сили, що перевищують їхні.

Коли вони використовували броньовий костюм браузера, вони отримували доступ до певного набору можливостей.

Коли вони використовували серверний костюм броні, вони отримали доступ до іншого набору можливостей.

Ці костюми мають певне перекриття, оскільки творці цих костюмів мали однакові потреби в певних місцях, а в інших - не.

Це те, що є середовище. Місце, де запускається код, де існують інструменти, побудовані поверх існуючої мови JavaScript. Вони не є частиною мови, але рядок часто розмита, оскільки ми використовуємо ці інструменти щодня, коли пишемо код.

setTimeout, fetch і DOM - все це приклади веб-API. (Ви можете переглянути повний список веб-API тут.) Це інструменти, які вбудовані в браузер і доступні нам під час запуску нашого коду.

І оскільки ми завжди запускаємо JavaScript у середовищі, здається, що це частина мови. Але вони не є.

Отже, якщо ви коли-небудь задавались питанням, чому ви можете використовувати вибірку в JavaScript під час запуску в браузері (але вам потрібно встановити пакет під час запуску в NodeJS), ось чому. Хтось вважав, що вибір - це гарна ідея, і побудував його як інструмент для середовища NodeJS.

Заплутаний? Так!

Але тепер ми нарешті можемо зрозуміти, що бере на себе робота з JavaScript і як її наймають.

Виявляється, саме середовище бере на себе роботу, і спосіб примусити середовище виконати цю роботу полягає у використанні функціональних можливостей, що належать до середовища. Наприклад, fetch або setTimeout у середовищі браузера.

Що відбувається з твором?

Чудово. Отже, середовище бере на себе роботу. Тоді що?

У якийсь момент вам потрібно повернути результати. Але давайте подумаємо, як це могло би працювати.

Повернемося до прикладу жонглювання з самого початку. Уявіть, що ви попросили новий м’яч, а друг просто почав кидати вам м’яч, коли ви не були готові.

Це було б катастрофою. Можливо, вам могло б пощастити, зловити це і ефективно ввести це у свій розпорядок дня. Але є велика ймовірність того, що це може призвести до того, що ви кинете всі м’ячі і розбиєте свою рутину. Чи не було б краще, якщо б ви дали чіткі вказівки щодо того, коли приймати м’яч?

Як виявляється, існують суворі правила, коли JavaScript може отримувати делеговану роботу.

Ці правила регулюються циклом подій і включають чергу мікрозадач та макрозадач. Так, я знаю. Це дуже багато. Але терпіть зі мною.

Гаразд Отже, коли ми делегуємо асинхронний код браузеру, браузер приймає та запускає код і бере на себе це навантаження. Але браузеру може бути надано кілька завдань, тому нам потрібно переконатися, що ми можемо визначити ці завдання пріоритетними.

Тут входять у чергу черга мікрозадач і черга макрозадач. Браузер візьме роботу, виконає її, а потім помістить результат в одну з двох черг, залежно від типу роботи, яку він отримує.

Наприклад, обіцянки розміщуються в черзі мікрозадач і мають вищий пріоритет.

Події та setTimeout - це приклади роботи, яка поміщається в чергу макрозадач і має нижчий пріоритет.

Тепер, коли робота закінчена і розміщена в одній з двох черг, цикл подій буде запускатися вперед і назад і перевіряти, чи готовий JavaScript до отримання результатів.

Тільки після завершення роботи JavaScript з усіма його синхронними кодами, якісного та готового, цикл подій почне вибиратись з черг та передавати функції назад у JavaScript для запуску.

Тож давайте подивимось на приклад:

setTimeout(() => console.log("hello"), 0) fetch("//someapi/data").then(response => response.json()) .then(data => console.log(data)) console.log("What soup?")

Який тут буде порядок?

  1. По-перше, setTimeout делегується браузеру, який виконує роботу та розміщує отриману функцію в черзі макрозадач.
  2. По-друге, вибір передається браузеру, який бере роботу. Він отримує дані з кінцевої точки і розміщує отримані функції в черзі мікрозадач.
  3. Javascript виходить із журналу "Який суп"?
  4. Цикл подій перевіряє, чи готовий JavaScript до отримання результатів роботи в черзі.
  5. When the console.log is done, JavaScript is ready. The event loop picks queued functions from the microtask queue, which has a higher priority, and gives them back to JavaScript to execute.
  6. After the microtask queue is empty, the setTimeout callback is taken out of the macrotask queue and given back to JavaScript to execute.
In console: // What soup? // the data from the api // hello

Promises

Now you should have a good deal of knowledge about how asynchronous code is handled by JavaScript and the browser environment. So let's talk about promises.

A promise is a JavaScript construct that represents a future unknown value. Conceptually, a promise is just JavaScript promising to return a value. It could be the result from an API call, or it could be an error object from a failed network request. You're guaranteed to get something.

const promise = new Promise((resolve, reject) => { // Make a network request if (response.status === 200) { resolve(response.body) } else { const error = { ... } reject(error) } }) promise.then(res => { console.log(res) }).catch(err => { console.log(err) })

A promise can have the following states:

  • fulfilled - action successfully completed
  • rejected - action failed
  • pending - neither action has been completed
  • settled - has been fulfilled or rejected

A promise receives a resolve and a reject function that can be called to trigger one of these states.

One of the big selling points of promises is that we can chain functions that we want to happen on success (resolve) or failure (reject):

  • To register a function to run on success we use .then
  • To register a function to run on failure we use .catch
// Fetch returns a promise fetch("//swapi.dev/api/people/1") .then((res) => console.log("This function is run when the request succeeds", res) .catch(err => console.log("This function is run when the request fails", err) // Chaining multiple functions fetch("//swapi.dev/api/people/1") .then((res) => doSomethingWithResult(res)) .then((finalResult) => console.log(finalResult)) .catch((err => doSomethingWithErr(err))

Perfect. Now let's take a closer look at what this looks like under the hood, using fetch as an example:

const fetch = (url, options) => { // simplified return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() // ... make request xhr.onload = () => { const options = { status: xhr.status, statusText: xhr.statusText ... } resolve(new Response(xhr.response, options)) } xhr.onerror = () => { reject(new TypeError("Request failed")) } } fetch("//swapi.dev/api/people/1") // Register handleResponse to run when promise resolves .then(handleResponse) .catch(handleError) // conceptually, the promise looks like this now: // { status: "pending", onsuccess: [handleResponse], onfailure: [handleError] } const handleResponse = (response) => { // handleResponse will automatically receive the response, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } const handleError = (response) => { // handleError will automatically receive the error, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } // the promise will either resolve or reject causing it to run all of the registered functions in the respective arrays // injecting the value. Let's inspect the happy path: // 1. XHR event listener fires // 2. If the request was successfull, the onload event listener triggers // 3. The onload fires the resolve(VALUE) function with given value // 4. Resolve triggers and schedules the functions registered with .then 

So we can use promises to do asynchronous work, and to be sure that we can handle any result from those promises. That is the value proposition. If you want to know more about promises you can read more about them here and here.

When we use promises, we chain our functions onto the promise to handle the different scenarios.

This works, but we still need to handle our logic inside callbacks (nested functions) once we get our results back. What if we could use promises but write synchronous looking code? It turns out we can.

Async/Await

Async/Await is a way of writing promises that allows us to write asynchronous code in a synchronous way. Let's have a look.

const getData = async () => { const response = await fetch("//jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data) } getData()

Nothing has changed under the hood here. We are still using promises to fetch data, but now it looks synchronous, and we no longer have .then and .catch blocks.

Async / Await is actually just syntactic sugar providing a way to create code that is easier to reason about, without changing the underlying dynamic.

Let's take a look at how it works.

Async/Await lets us use generators to pause the execution of a function. When we are using async / await we are not blocking because the function is yielding the control back over to the main program.

Then when the promise resolves we are using the generator to yield control back to the asynchronous function with the value from the resolved promise.

You can read more here for a great overview of generators and asynchronous code.

In effect, we can now write asynchronous code that looks like synchronous code. Which means that it is easier to reason about, and we can use synchronous tools for error handling such as try / catch:

const getData = async () => { try { const response = await fetch("//jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data) } catch (err) { console.log(err) } } getData()

Alright. So how do we use it? In order to use async / await we need to prepend the function with async. This does not make it an asynchronous function, it merely allows us to use await inside of it.

Failing to provide the async keyword will result in a syntax error when trying to use await inside a regular function.

const getData = async () => { console.log("We can use await in this function") }

Because of this, we can not use async / await on top level code. But async and await are still just syntactic sugar over promises. So we can handle top level cases with promise chaining:

async function getData() { let response = await fetch('//apiurl.com'); } // getData is a promise getData().then(res => console.log(res)).catch(err => console.log(err); 

This exposes another interesting fact about async / await. When defining a function as async, it will always return a promise.

Using async / await can seem like magic at first. But like any magic, it's just sufficiently advanced technology that has evolved over the years. Hopefully now you have a solid grasp of the fundamentals, and can use async / await with confidence.

Conclusion

If you made it here, congrats. You just added a key piece of knowledge about JavaScript and how it works with its environments to your toolbox.

This is definitely a confusing subject, and the lines are not always clear. But now you hopefully have a grasp on how JavaScript works with asynchronous code in the browser, and a stronger grasp over both promises and async / await.

If you enjoyed this article, you might also enjoy my youtube channel. I currently have a web fundamentals series going where I go through HTTP, building web servers from scratch and more.

There's also a series going on building an entire app with React, if that is your jam. And I plan to add much more content here in the future going in depth on JavaScript topics.

And if you want to say hi or chat about web development, you could always reach out to me on twitter at @foseberg. Thanks for reading!