Введение в асинхронный JavaScript

Этот перевод не завершён. Пожалуйста, помогите перевести эту статью с английского

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

Необходимое условие: Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель: Ознакомиться с тем, что такое асинхронный JavaScript, чем он отличается от синхронного и в каких случаях используется.

Синхронный JavaScript

Чтобы (позволить нам) понять что есть асинхронный JavaScript, нам следовало бы для начала убедиться, что мы понимаем что такое синхронный JavaScript. Этот раздел резюмирует некоторую информацию из прошлой статьи.

Большая часть функционала, который мы рассматривали в предыдущих обучающих модулях - синхронный; вы запускаете какой-то код, а результат возвращается как только браузер может его вернуть. Давайте рассмотрим простой пример ( посмотрите онлайн как это работает и посмотрите исходный код):

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

В этом блоке кода команды выполняются одна за другой:

  1. Получаем ссылку на элемент <button>, который уже есть в DOM.
  2. Добавляем к кнопке обработчик события click так что при нажатии на неё:
    1. Выводим сообщение alert().
    2. После закрытия сообщения создаём элемент <p> (абзац).
    3. атем добавляем в абзац текст.
    4. В конце добавляем созданный абзац в тело документа.

Пока выполняется каждая операция, ничего больше не может произойти — обработка (отображение) документа приостановлена. Так происходит, как было сказано в предыдущей статье, потому что JavaScript является однопоточным. В каждый момент времени может выполняться только одна команда, обрабатываемая в единственном - главном потоке. Все остальные действия блокируются до окончания выполнения текущей команды.

Так и в примере выше: после нажатия кнопки абзац не сможет появиться пока не будет нажата кнопка OK в окне сообщения. Попробуйте сами:

Note: Важно помнить, что alert(), хоть и часто используется для демонстрации синхронных блокирующих операций, сильно не рекомендован к использованию в реальных приложениях.

Асинхронный JavaScript

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

Почему бы не использовать синхронный код? Давайте посмотрим на небольшой пример. Когда вы получаете картинку из сервера, вы не можете мгновенно вернуть результат. Это значит что следующий (псевдокод) не сработает:

var response = fetch('myImage.png');
var blob = response.blob();
// display your image blob in the UI somehow

Это проиходит потому что вы не знаете сколько времени займет загрузка картинки, следовательно когда вы начинаете выполнять вторую строку кода, сгенерируется ошибка ( возможно с перерывами,либо каждый раз) потому что response еще не доступен. Вместо этого, ваш код должен подождать возвращения response до того,как попытается выполнить дальнешие инструкции.

Есть два типа асинхронного кода с которыми вы столкнетесь в коде JavaScript, старый метод - callbacks и более новый - promise. В следующих разделах мы познакомимся с каждый из них. 

Асинхронные коллбэки

Асинхронные коллбэки это функции, которые определяются как агрументы при вызове функции, которая начнет выполнение кода на заднем фоне. Когда код на заднем фоне завершает свою работу, он вызвает коллбэк функцию оповещающую что работа сделана, либо оповещающую о трудностях в завершении работы. Использование коллбэков немного устраревшая практика, но они все еще употребляются в некоторых старомодных, но часто используемых APIs.

Пример асинхронного коллбэка вторым параметром addEventListener() (как мы видели в предыдущем действии):

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

Первый параметр - тип прослушиваемого события, второй параметр - коллбэк функция вызываемая при срабатывании события.

При передаче коллбэк функциии как аргумента в другую функцию, мы передаем только ссылку на функцию как аргумента, следовательно коллбэк функция не выполняется мгновенно. Где-то существует обратный вызов (отсюда и название) выполняющийся асинхронно внутри тела, содержащего функцию. Эта функиця должна выполнять коллбэк функцию в нужный момент.

Вы можете очень просто написать свою собственную функцию содержащую коллбэк. Давайте взглянем на другой пример, в котором происходит загрузка ресурсов через XMLHttpRequest API (run it live, and see the source):

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = type;

  xhr.onload = function() {
    callback(xhr.response);
  };

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

Мы создали  функцию displayImage(), которая представляет blob, переданный в нее как обьект URL, и создает картинку, в которой отображается URL , добавляя ее в елемент документа <body>. Однако, далее мы создаем функцию loadAsset() ,которая принимает коллбэк как параметр, вместе с URL для получения данных и тип контента. Для получения данных из URL используется XMLHttpRequest (часто сокращается до аббревиатуры "XHR") , перед тем как передать ответ в коллбэк для дальнейшей обработки. В этом случае коллбэк ждет пока XHR закончит загрузку данных (используя обрабочик события onload) перед отправкой данных в коллбэк.

Коллбэки универсальны - они не только позволяют вам контролировать порядок, в котором запускаются функции и данные передающиеся между ними, они также позволяют передавать данные к различным функциям, в зависимости от обстоятельств. Вы можете выполнять различные действия с загруженным ответом, такие как  processJSON(), displayText(), и другие.

Заметьте, что не все коллбэки асинхронны - некторые запускаются синхронно. Например при использовании Array.prototype.forEach() для перебора айтемов в массиве (see it live, and the source):

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

В этом примере мы перебираем массив с именами греческих богов . Ожидаемый параметр для forEach() это коллбэк функция, которая несет в себе два параметра, ссылки на имя массива и значения индекса. Однако эта функция не ожидает никаких действий выполняемых над ней - она запускается немедленно.

Promises

Promises are the new style of async code that you'll see used in modern Web APIs. A good example is the fetch() API, which is basically like a modern, more efficient version of XMLHttpRequest. Let's look at a quick example, from our Fetching data from the server article:

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

Note: You can find the finished version on GitHub (see the source here, and also see it running live).

Here we see fetch() taking a single parameter — the URL of a resource you want to fetch from the network — and returning a promise. The promise is an object representing the completion or failure of the async operation. It represents an intermediate state, as it were. In essence, it's the browser's way of saying "I promise to get back to you with the answer as soon as I can," hence the name "promise."

This concept can take practice to get used to; it feels a little like Schrödinger's cat in action. Neither of the possible outcomes have happened yet, so the fetch operation is currently waiting on the result of the browser trying to complete the operation at some point in the future. We've then got three further code blocks chained onto the end of the fetch():

  • Two then() blocks. Both contain a callback function that will run if the previous operation is successful, and each callback receives as input the result of the previous successful operation, so you can go forward and do something else to it. Each .then() block returns another promise, meaning that you can chain multiple .then() blocks onto each other, so multiple asynchronous operations can be made to run in order, one after another.
  • The catch() block at the end runs if any of the .then() blocks fail — in a similar way to synchronous try...catch blocks, an error object is made available inside the catch(), which can be used to report the kind of error that has occurred. Note however that synchronous try...catch won't work with promises, although it will work with async/await, as you'll learn later on.

Note: You'll learn a lot more about promises later on in the module, so don't worry if you don't understand them fully yet.

Очередь событий

Async operations like promises are put into an event queue, which runs after the main thread has finished processing so that they do not block subsequent JavaScript code from running. The queued operations will complete as soon as possible then return their results to the JavaScript environment.

Promises versus callbacks

Promises have some similarities to old-style callbacks. They are essentially a returned object to which you attach callback functions, rather than having to pass callbacks into a function.

However, promises are specifically made for handling async operations, and have many advantages over old-style callbacks:

  • You can chain multiple async operations together using multiple .then() operations, passing the result of one into the next one as an input. This is much harder to do with callbacks, which often ends up with a messy "pyramid of doom" (also known as callback hell).
  • Promise callbacks are always called in the strict order they are placed in the event queue.
  • Error handling is much better — all errors are handled by a single .catch() block at the end of the block, rather than being individually handled in each level of the "pyramid".
  • Avoid inversion control: unlike callbacks will lose full control of how the function will be executed when passing a callback to a third-party library. A great demonstration by Stevie Jay.

The nature of asynchronous code

Let's explore an example that further illustrates the nature of async code, showing what can happen when we are not fully aware of code execution order and the problems of trying to treat asynchronous code like synchronous code. The following example is fairly similar to what we've seen before (see it live, and the source). One difference is that we've included a number of console.log() statements to illustrate an order that you might think the code would execute in.

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');

The browser will begin executing the code, see the first console.log() statement (Starting) and execute it, and then create the image variable.

It will then move to the next line and begin executing the fetch() block but, because fetch() executes asynchronously without blocking, code execution continues after the promise-related code, thereby reaching the final console.log() statement (All done!) and outputting it to the console.

Only once the fetch() block has completely finished running and delivering its result through the .then() blocks will we finally see the second console.log() message (It worked ;)) appear. So the messages have appeared in a different order to what you might expect:

  • Starting
  • All done!
  • It worked :)

If this confuses you, then consider the following smaller example:

console.log("registering click handler");

button.addEventListener('click', () => {
  console.log("get click");
});

console.log("all done");

This is very similar in behavior — the first and third console.log() messages will be shown immediately, but the second one is blocked from running until someone clicks the mouse button. The previous example works in the same way, except that in that case the second message is blocked on the promise chain fetching a resource then displaying it on screen, rather than a click .

In a less trivial code example, this kind of setup could cause a problem — you can't include an async code block that returns a result, which you then rely on later in a sync code block. You just can't guarantee that the async function will return before the browser has processed the async block.

To see this in action, try taking a local copy of our example, and changing the third console.log() call to the following:

console.log ('All done! ' + image + 'displayed.');

You should now get an error in your console instead of the third message:

TypeError: image is undefined; can't access its "src" property

This is because at the time the browser tries to run the third console.log() statement, the fetch() block has not finished running so the image variable has not been given a value.

Active learning: make it all async!

To fix the problematic fetch() example and make the three console.log() statements appear in the desired order, you could make the third console.log() statement run async as well. This can be done by moving it inside another .then() block chained onto the end of the second one, or by simply moving it inside the second then() block. Try fixing this now.

Note: If you get stuck, you can find an answer here (see it running live also). You can also find a lot more information on promises in our Graceful asynchronous programming with Promises guide, later on in the module.

Conclusion

In its most basic form, JavaScript is a synchronous, blocking, single-threaded language, in which only one operation can be in progress at a time. But web browsers define functions and APIs that allow us to register functions that should not be executed synchronously, and should instead be invoked asynchronously when some kind of event occurs (the passage of time, the user's interaction with the mouse, or the arrival of data over the network, for example). This means that you can let your code do several things at the same time without stopping or blocking your main thread.

Whether we want to run code synchronously or asynchronously will depend on what we're trying to do.

There are times when we want things to load and happen right away. For example when applying some user-defined styles to a webpage you'll want the styles to be applied as soon as possible.

If we're running an operation that takes time however, like querying a database and using the results to populate templates, it is better to push this off the main thread and complete the task asynchronously. Over time, you'll learn when it makes more sense to choose an asynchronous technique over a synchronous one.

In this module