[Перевод] Упрощение асинхронного кода на JavaScript с внедрением асинхронных функций из ES2016
Хотя мы еще продолжаем работу над внедрением поддержки ES6/2015, команда Chackra также смотри за пределы ES2016 и, в частности, на асинхронные функции. Мы рады объявить об экспериментальной поддержке async-функций в Microsoft Edge, начиная со сборки Microsoft Edge (EdgeHTML 13.10547).
Асинхронные функции в 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, используя диаграмму, представленную ниже. Части, потребовавшие наибольших изменений для поддержки асинхронных функций отмечены зеленым.
Глобальное поведение
Использование ключевого слова 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”, как показано ниже:
Асинхронные функции доступны в превью-режиме в рамках программы Windows Insider, начиная со сборки Microsoft Edge 13.10547. Будем рады услышать ваши отзывы по использованию данной функциональности в вашем коде в нашем Twitter @MSEdgeDev или через Connect.
– Etienne Baudoux, Software Development Engineer Intern, Chakra Team
– Brian Terlson, Senior Program Manager, Chakra Team