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 и как се наема.

Оказва се, че средата е тази, която поема работата, а начинът да се накара средата да свърши тази работа е да се използва функционалност, която принадлежи на средата. Например извличане или 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!