Путешествие JavaScript в город асинхронности
Благодаря усилиям TC39 — организации, ответственной за стандартизацию JavaScript (ECMAScript, если быть точным) — JavaScript прошёл достаточно длинный путь от первых версий и стал современным, широко используемым языком.
В частности в ECMAScript была значительно улучшена асинхронность (что такое асинхронное программирование?). Кстати, в новом браузере Edge из Windows 10 мы добавили поддержку современных возможностей для работы с асинхронным кодом. Можете посмотреть полный список нововведений:
https://dev.modern.ie/platform/changelog/desktop/10547/?compareWith=10532
Список довольно длинный, но в этой статье мы сфокусируемся на асинхронных функциях (ES2016 Async Functions). Мы совершим путешествие сквозь все нововведения, связанные с асинхронностью, а также поймём, как они могут улучшить ваш код.
Первая остановка: ECMAScript 5 – город коллбеков
Вся работа с асинхронностью в ECMAScript 5 (и предыдущих версиях) сводилась к коллбекам. Проиллюстрирую это простой задачей, с которой вы, вероятно, сталкиваетесь довольно часто: выполнение XHR-запроса.
var displayDiv = document.getElementById("displayDiv");
// Объявляем функцию для обработки результата
var processJSON = function (json) {
var result = JSON.parse(json);
result.collection.forEach(function(card) {
var div = document.createElement("div");
div.innerHTML = card.name + " cost is " + card.price;
displayDiv.appendChild(div);
});
}
// Объявляем функцию для отображения ошибок
var displayError = function(error) {
displayDiv.innerHTML = error;
}
// Создаём и настраиваем XHR-объект
var xhr = new XMLHttpRequest();
xhr.open('GET', "cards.json");
// Объявляем коллбеки, которые будут вызваны XHR-объектом
xhr.onload = function(){
if (xhr.status === 200) {
processJSON(xhr.response);
}
}
xhr.onerror = function() {
displayError("Не получилось загрузить RSS");
}
// Отправляем запрос
xhr.send();
Опытным JavaScript-разработчикам знаком подобный код, потому что коллбеки вместе c XHR-запросами используются повсеместно. Принцип довольно прост: разработчик создаёт запрос, а затем задаёт коллбек нужному XHR-объекту.
С другой стороны, коллбеки трудны для понимания из-за внутренней природы асинхронного кода:
Всё станет ещё сложнее, если внутри коллбека создать ещё один асинхронный запрос — вы окажетесь в так называемом «аду коллбеков».
Вторая остановка: ECMAScript 6 – город промисов
ECMAScript 6 набирает обороты, и Edge на данный момент лидирует среди браузеров с 88% поддержкой возможностей нового стандарта.
Помимо прочих прекрасных улучшений, в ECMAScript 6 стандартизировали использование промисов (ранее известных как futures).
Согласно определению с MDN, промис — это объект, используемый для выполнения отложенных и асинхронных вычислений. Промис представляет операцию, которая ещё не была завершена, но её завершение ожидается в будущем. Промисы позволяют организовать выполнение асинхронных операций так, будто эти операции синхронны. Как раз то, что нужно для примера с XHR-запросом.
Промисы существовали ещё до появления стандарта ES6, но благодаря их стандартизации больше не придётся использовать сторонние библиотеки — теперь промисы поддерживаются современными браузерами «из коробки».
Давайте перепишем предыдущий пример используя промисы и посмотрим, как они делают код более читабельным и поддерживаемым:
var displayDiv = document.getElementById("displayDiv");
// Создаём функцию, возвращающую промис
function getJsonAsync(url) {
// Промисам требуется две функции: одна для обработки успешного
// завершения операции, вторая для обработки ошибок
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
// Запрос выполнился успешно
resolve(xhr.response);
} else {
// Запрос не выполнился, обрабатываем ошибку
reject("Не получилось загрузить RSS");
}
}
xhr.onerror = () => {
// Запрос не выполнился, обрабатываем ошибку
reject("Не получилось загрузить RSS");
};
xhr.send();
});
}
// Функция возвращает промис, поэтому мы можем создавать цепочку
// вызовов .then и .catch
getJsonAsync("cards.json").then(json => {
var result = JSON.parse(json);
result.collection.forEach(card => {
var div = document.createElement("div");
div.innerHTML = `${card.name} cost is ${card.price}`;
displayDiv.appendChild(div);
});
}).catch(error => {
displayDiv.innerHTML = error;
});
Наверняка вы заметили множество улучшений. Рассмотрим их подробнее.
Создание промиса
Чтобы «промисифицировать» (простите, я француз, так что позвольте мне придумывать новые слова) XHR, вам нужно создать новый объект Promise:
Использование промиса
После создания промиса можно строить элегантные цепочки асинхронных вызовов:
Таким образом код (с точки зрения пользователя) выглядит так:
- Получаем промис (1)
- Начинаем цепочку с обработки успешного завершения операции (2 and 3)
- Заканчиваем цепочку обработкой ошибки (4) (аналогично try/catch)
Что действительно интересно — можно запросто создавать цепочки промисов,
вызывая .then().then()
и так далее.
Примечание: вы могли заметить, что помимо промисов я использовал также некоторый синтаксический сахар из ECMAScript 6 вроде строковых шаблонов или стрелочных функций.
Конечная: ECMAScript 7 — город асинхронности
Итак, мы достигли точки назначения! Мы практически в будущем, но благодаря команде разработчиков браузера Edge в одной из его последних сборок появилась поддержка асинхронных функций!
Асинхронные функции — это синтаксический сахар, на уровне языка улучшающий работу с асинхронным кодом.
В основе асинхронных функций лежат современные возможности ECMAScript 6 вроде генераторов. На самом деле, генераторы вместе с промисами позволяют сымитировать поведение асинхронных функций, но для этого потребуется написать намного больше кода.
Нам не требуется изменять функцию, генерирующую промис, потому что асинхронные функции работают напрямую с промисами.
Нам нужно лишь изменить вызов функции:
// Создаём анонимную асинхронную функцию
(async function() {
try {
// Просто дожидаемся результата из промиса
var json = await getJsonAsync("cards.json");
var result = JSON.parse(json);
result.collection.forEach(card => {
var div = document.createElement("div");
div.innerHTML = `${card.name} cost is ${card.price}`;
displayDiv.appendChild(div);
});
} catch (e) {
displayDiv.innerHTML = e;
}
})();
Вот где начинается магия. Этот код написан в синхронном стиле с прямым порядком выполнения:
Впечатляет, не так ли?
И да, вы можете использовать асинхронные функции вместе со стрелочными функциями или методами классов.
Двигаемся дальше
Если вам интересно узнать как мы реализовали поддержку асинхронных функций в движке Chakra, то прочитайте анонс в официальном блоге Edge.
Вы также можете отслеживать поддержку браузерами новых возможностей ECMAScript 6 и 7 на сайте Kangax. Ну и не стесняйтесь проверять наш план по реализации новых возможностей в браузере Edge!
Не стесняйтесь давать нам обратную связь и голосовать за те возможности, которые вы в первую очередь хотите увидеть в браузере Edge:
Спасибо за внимание! Будем рады услышать ваши отзывы и идеи.