Асинхронные функции в ES7/ES2016
Ключевые слова async и await, как часть предложения по внедрению асинхронных функций, нацелены на упрощение написания асинхронного кода. Это одна из ключевых возможностей современного C# и часто запрашиваемая опция со стороны JavaScript-разработчиков. До введения асинхронных функций и промисов (promise) JS-разработчику приходилось оборачивать весь асинхронный код в отдельные от синхронного кода функции и использовать функции обратного вызова (callback) для работы с результатом асинхронного вычисления. Такой код довольно быстро становится трудно читать и поддерживать.
Промисы из ES, пройдя через стандартизацию и получая все большую прямую поддержку в браузерах, помогли улучшить способ написания асинхронного кода, но не решили проблему полностью, так как потребность в написании функций обратного вызова никуда не исчезла.
Асинхронные функции базируются на промисах и позволяют сделать следующий шаг. Когда вы добавляете ключевое слово async к функции или стрелочной функции, она автоматически будет возвращать промис. К примеру, следующий код – это типовая программа на ES2015, делающая http-запрос с помощью промисов:
// ES6 code, without async/await
function httpGet(url) {
return new Promise(function (resolve, reject) {
// do the usual Http request
var request = new XMLHttpRequest();
request.open('GET', url);
request.onload = function () {
if (request.status == 200) {
resolve(request.response);
} else {
reject(Error(request.statusText));
}
};
request.onerror = function () {
reject(Error('Network Error'));
};
request.send();
});
}
function httpGetJson(url) {
return new Promise(function (resolve, reject) {
// check if the URL looks like a JSON file and call httpGet.
var regex = /\.(json)$/i;
if (regex.test(url)) {
// call the promise, wait for the result
resolve(httpGet(url).then(function (response) {
return response;
}, function (error) {
reject(error);
}));
} else {
reject(Error('Bad File Format'));
}
});
}
httpGetJson('file.json').then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
Если мы перепишем этот же код, сохранив его поведение, с помощью асинхронных функций, то результат будет компактнее и проще считываться. Код ниже также включает небольшой рефакторинг в части обработки ошибки (обратите внимание на функцию httpGetJson):
// ES7 code, with async/await
function httpGet(url) {
return new Promise(function (resolve, reject) {
// do the usual Http request
let request = new XMLHttpRequest();
request.open('GET', url);
request.onload = function () {
if (request.status == 200) {
resolve(request.response);
} else {
reject(Error(request.statusText));
}
};
request.onerror = function () {
reject(Error('Network Error'));
};
request.send();
});
}
async function httpGetJson(url) {
// check if the URL looks like a JSON file and call httpGet.
let regex = /\.(json)$/i;
if (regex.test(url)) {
// call the async function, wait for the result
return await httpGet(url);
} else {
throw Error('Bad Url Format');
}
}
httpGetJson('file.json').then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
Ключевое слово async также работает со стрелочными функциями ES6, достаточно просто добавить ключевое слово перед аргументами. Вот небольшой пример:
// call the async function, wait for the result
let func = async () => await httpGet(url);
return await func();
Итого:
- Используйте ключевое слово async при определении любой функции или стрелочной функции, чтобы получить асинхронный код с промисом. Это включает также функции в классах и статичные функции. В последнем случае ключевое слово async нужно указывать после слова static и, соответственно, перед именем функции.
- Используйте ключевое слово await, чтобы ход выполнения дождался завершения async-выражения (к примеру, вызова async-функции) и получило значение из промиса.
- Если вы не используете ключевое слово await, вы получите сам промис.
- Вы не можете использовать ключевое слово await вне async-функции, в том числе его нельзя использовать в глобальном пространстве.
Как это реализовано в Chakra?
В одной из предыдущих статей мы обсуждали архитектуру движка Chakra, используя диаграмму, представленную ниже. Части, потребовавшие наибольших изменений для поддержки асинхронных функций отмечены зеленым.
![](https://habrastorage.org/getpro/habr/post_images/ba6/5b0/b23/ba65b0b23091a6a33b49c5a1444dbe2b.png)
Глобальное поведение
Использование ключевого слова async генерирует конструктор промиса, который, в соответствии со спецификацией, является оберткой вокруг содержимого функции. Для выполнения этого действия, генератор байт-кода Chakra формирует вызов встроенной функции, реализующей следующее поведение:
function spawn(genF, self) {
return new Promise(function (resolve, reject) {
var gen = genF.call(self);
function step(nextF) {
var next;
try {
next = nextF();
} catch (e) {
// finished with failure, reject the promise
reject(e);
return;
}
if (next.done) {
// finished with success, resolve the promise
resolve(next.value);
return;
}
// not finished, chain off the yielded promise and `step` again
Promise.resolve(next.value).then(function (v) {
step(function () { return gen.next(v); });
}, function (e) {
step(function () { return gen.throw(e); });
});
}
step(function () { return gen.next(undefined); });
});
}
Порождающая функция, приведенная выше, спроектирована так, чтобы обработать все async-выражения в теле функции и решить, нужно ли продолжить или остановить процесс в зависимости от поведения внутри async-функции. Если async-выражение, вызванное с ключевым словом await, проваливается, например, вследствие возникновения ошибки внутри async-функции или в целом ожидаемого выражения, то промис возвращает отказ, который можно обработать выше по стеку.
Далее движок должен вызвать порождающую функцию из JS-скрипта, чтобы получить промис и выполнить содержимое функции. Чтобы это сделать, когда парсер находит ключевое слово async, движок изменяет AST (абстрактное синтаксическое дерево), представляющее алгоритм, чтобы добавить вызов spawn-функции с телом целевой функции. Как следствие, функция httpGetJson из примера выше конвертируется парсером примерно следующим образом:
function httpGetJson(url) {
return spawn(function* () {
// check if the URL looks like a JSON file and call httpGet.
var regex = /\.(json)$/i;
if (regex.test(url)) {
// call the async function, wait for the result
return yield httpGet(url);
} else {
throw Error('Bad Url Format');
}
}, this);
}
Обратите внимание на использование генераторов и ключевого слова yield для реализации поведения ключевого слова await. На самом деле, реализация поддержки ключевого слова await очень похожа на работу с ключевым словом yield.
Поведение с аргументом по умолчанию
Одна из новых возможностей ES6 – это установка значения по умолчанию для аргумента функции. Когда используется значение по умолчанию, генератор байт-кода установит это значение в начале тела функции.
// original JavaScript code
function foo(argument = true) {
// some stuff
}
// representation of the Bytecode Generator's output in JavaScript
function foo(argument) {
argument = true;
// some stuff
}
В случае использования ключевого слова async, если значение по умолчанию приводит к возникновению ошибки (исключения), спецификация требует отказать в выполнении промиса. Это позволяет с легкостью отлавливать исключения.
Чтобы реализовать это в Chakra, у команды был выбор из двух вариантов: изменить AST, или реализовать такое поведение напрямую в генераторе байт-кода. Мы выбрали второе и передвинули инициализацию аргументов в начало тела функции напрямую в байт-коде, так как это более простое и понятное решение в рамках нашего движка. Так как для перехвата ошибок из значения по умолчанию нужно было добавить блок try/catch, то нам было проще напрямую изменить байт-код при обнаружении ключевого слова async.
Наконец, сгенерированный байт-код будет напоминать результат, создаваемый для такого кода на JavaScript:
// representation of the Bytecode Generator's output in JavaScript
function foo(argument) {
try {
argument = true;
} catch (error) {
return Promise.reject(error);
}
return spawn(function* () { // keep this call as we are in an async function
// some stuff
}, this);
}
Как включить поддержку асинхронных функций Microsoft Edge?
Чтобы включить экспериментальную поддержку асинхронных функций в Microsoft Edge, перейдите на страницу about:flags в Microsoft Edge и выберите опцию “Enable experimental JavaScript features”, как показано ниже:
![](https://habrastorage.org/getpro/habr/post_images/3fd/dd4/307/3fddd4307f97f431e3f0755492c2763a.png)
Асинхронные функции доступны в превью-режиме в рамках программы Windows Insider, начиная со сборки Microsoft Edge 13.10547. Будем рады услышать ваши отзывы по использованию данной функциональности в вашем коде в нашем Twitter @MSEdgeDev или через Connect.
– Etienne Baudoux, Software Development Engineer Intern, Chakra Team
– Brian Terlson, Senior Program Manager, Chakra Team
Комментарии (18)
justboris
30.10.2015 23:37+5Пример без async-функции искусственно усугубляет ситуацию. Я бы написал функцию httpGetJson вот так:
function httpGetJson(url) { // check if the URL looks like a JSON file and call httpGet. var regex = /\.(json)$/i; if (regex.test(url)) { return httpGet(url); } else { return Promise.reject(Error('Bad File Format')); } }
Получилось ровно столько же строк, что и в async-варианте.
Перед тем, как мечтать о волшебных async-функциях, стоит внимательнее изучить то, что у нас уже есть. Например, прочитав недавнюю статью о правильном использовании Promise.ankh1989
31.10.2015 06:28-4Очень уж много буков в вашей функции:
const httpGetJson = url => /\.(json)$/i.test(url) ? httpGet(url) : Promise.reject(Error('Bad File Format'));
Но так, имхо, намного лучше:
const httpGetJson = async url => /\.(json)$/i.test(url) ? await httpGet(url) : throw Error('Bad File Format');
arvitaly
31.10.2015 07:52+3Проблемы начинаются, когда внутри одной функции нужно несколько последовательных асинхронных вызовов. Конечно, можно разбивать все это на разные функции, но это означает, что мы думаем не об архитектуре, а о синтаксических возможностях.
Также, можно использовать Promise, then, но тогда нужно писать огромное количество шаблонного кода:
asyncPromise1().then(function(){ return asyncPromise2(); }).then(function(arg2){ console.log(arg2); });
VS
await asyncPromise1(); console.log(await asyncPromise2())
MuLLtiQ
01.11.2015 03:58asyncPromise1() .then(asyncPromise2) .then(console.log);
arvitaly
01.11.2015 09:39К сожалению, ваш пример не рабочий, как минимум нужно записать так
asyncPromise1() .then(asyncPromise2) .then(console.log.bind(console));
К тому же, ваш вариант просто проглатывает возможные ошибки (нет catch), а async/await вызовут Error.
Но для продолжения дискуссии, предлагаю написать вариант еще и для такого кода:
await asyncPromise1(); console.log(await asyncPromise2() + await asyncPromise3());
MuLLtiQ
01.11.2015 15:02Так я просто переписал первый кусок кода с промисами из вашего комментария в более читабельном виде. В node, кстати console.log биндить к console не нужно, но в браузерах, конечно же, не так. Но на самом-то деле я считаю что async/await это очень хорошо и красиво и поскорее бы они были везде.
Насчет вашей задачи: я бы сделал как-то так:
var asyncPromise1 = () => Promise.resolve(); var asyncPromise2 = () => Promise.resolve('foo'); var asyncPromise3 = () => Promise.resolve('bar'); asyncPromise1() .then(asyncPromise2) .then(arg2 => asyncPromise3() .then(arg3 => arg2 + arg3)) .then(console.log.bind(console)) // "foobar" .catch(err => console.log(err));
Читабельность тут, по сравнению с async/await, конечно же, хуже, но я все еще могу понять что здесь происходит.
MaximChistov
31.10.2015 10:29-5А планируете что-то сделать с «замечательной» фичей, когда случайно вытаскиваешь вкладку в отдельное окно, перетаскиваешь ее назад, а окно, в которое она выделилась, уже без вкладок, так и висит незакрытым? Раздражает.
Liumee
01.11.2015 12:34Ожидал увидеть использование Fetch API в примерах… По-моему, оно хорошо вписывается в ES6-код — такое же элегантное.
Finom
Асинхронные функции — это самое крутое нововведение ECMAScript.next. Я очень удивлен, что компания, создавшая Internet Explorer 6 первой реализовала их. Я польщен.
![image](https://habrastorage.org/getpro/habr/comment_images/530/82a/47c/53082a47cc1183c3fe09fd28dcdb7491.gif)
Fedcomp
Ну если подумать то таже самая компания создала IE 5 в том числе.
prairie_dog
Что было плохого в IE6 на момент выхода?
Finom
На момент выхода — ничего. Проблема в том, что они не убирали поддержку браузера до тех пор, пока все разработчики не возненавидели Осла. Сейчас они молодцы, внедляют всякие крутые штуки и фиксят баги достаточно быстро. Еще бы по-быстрее IE 9-11 ушли на пенсию, мир бы стал добрее.
justboris
Стандарт async-функций еще не утвержден. Если в нем к моменту выхода что-то поменяется, то мы получим два разных async: в нормальных браузерах по стандарту и особое поведение в IE.
Зачем спешить реализовывать нестабильные фичи, если есть уже сделанные в других браузерах, ServiceWorkers, например.
Kalifriki
В Edge async-await будет доступен пока только под флагом.
rock
Текущая стадия предложения асинхронных функций 3 — кандидат — рекомендовано для имплементации в движках для получения обратной связи от имплементаторов и пользователей, но не для использования в рабочих проектах. Кстати, ServiceWorkers тоже пока только на стадии черновика.