Если вы следили за миром JavaScript, вы, вероятно, слышали об обещаниях. Если вы хотите узнать об обещаниях , в Интернете есть несколько отличных учебных пособий, но я не буду их здесь объяснять; В этой статье предполагается, что у вас уже есть знания об обещаниях.
Обещания рекламируются как будущее асинхронного программирования в JavaScript. Обещания действительно хороши и помогают решить множество проблем, возникающих при асинхронном программировании, но это утверждение является лишь несколько правильным. В действительности, обещания являются основой будущего асинхронного программирования в JavaScript. В идеале обещания будут скрыты за кулисами, и мы сможем написать наш асинхронный код, как если бы он был синхронным.
В ECMAScript 7 это станет чем-то большим, чем просто фантастическая мечта: она станет реальностью, и я покажу вам эту реальность — так называемые асинхронные функции — прямо сейчас. Почему мы говорим об этом сейчас? В конце концов, ES6 даже не была окончательно доработана, так что кто знает, сколько пройдет времени, прежде чем мы увидим ES7. Правда в том, что вы можете использовать эту технологию прямо сейчас, и в конце этой статьи я покажу вам, как это сделать.
Текущее состояние дел
Прежде чем я начну демонстрировать, как использовать асинхронные функции, я хочу рассмотреть некоторые примеры с обещаниями (используя обещания ES6). Позже я приведу эти примеры для использования асинхронных функций, чтобы вы могли увидеть, насколько это важно.
Примеры
Для нашего первого примера мы сделаем что-то действительно простое: вызов асинхронной функции и запись возвращаемого значения.
1
2
3
4
5
6
7
|
function getValues() {
return Promise.resolve([1,2,3,4]);
}
getValues().then(function(values) {
console.log(values);
});
|
Теперь, когда у нас есть определенный базовый пример, давайте перейдем к чему-то более сложному. Я буду использовать и модифицировать примеры из поста в моем собственном блоге, в котором рассматриваются некоторые шаблоны для использования обещаний в различных сценариях. Каждый из примеров асинхронно извлекает массив значений, выполняет асинхронную операцию, которая преобразует каждое значение в массиве, регистрирует каждое новое значение и, наконец, возвращает массив, заполненный новыми значениями.
Сначала мы рассмотрим пример, в котором несколько асинхронных операций будут выполняться параллельно, а затем сразу же ответим на них, когда каждая из них завершится, независимо от порядка их завершения. Функция getValues
такая же, как и в предыдущем примере. Функция asyncOperation
также будет повторно использована в следующих примерах.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function asyncOperation(value) {
return Promise.resolve(value + 1);
}
function foo() {
return getValues().then(function(values) {
var operations = values.map(function(value) {
return asyncOperation(value).then(function(newValue) {
console.log(newValue);
return newValue;
});
});
return Promise.all(operations);
}).catch(function(err) {
console.log(‘We had an ‘, err);
});
}
|
Мы можем сделать то же самое, но убедитесь, что регистрация происходит в порядке элементов в массиве. Другими словами, этот следующий пример будет выполнять асинхронную работу параллельно, но синхронная работа будет последовательной:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
function foo() {
return getValues().then(function(values) {
var operations = values.map(asyncOperation);
return Promise.all(operations).then(function(newValues) {
newValues.forEach(function(newValue) {
console.log(newValue);
});
return newValues;
});
}).catch(function(err) {
console.log(‘We had an ‘, err);
});
}
|
В нашем последнем примере будет продемонстрирован шаблон, в котором мы ожидаем завершения предыдущей асинхронной операции, прежде чем начинать следующую. В этом примере ничего не работает параллельно; все последовательно.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
function foo() {
var newValues = [];
return getValues().then(function(values) {
return values.reduce(function(previousOperation, value) {
return previousOperation.then(function() {
return asyncOperation(value);
}).then(function(newValue) {
console.log(newValue);
newValues.push(newValue);
});
}, Promise.resolve()).then(function() {
return newValues;
});
}).catch(function(err) {
console.log(‘We had an ‘, err);
});
}
|
Даже с возможностью обещаний уменьшить вложенность обратного вызова, это не очень помогает. Выполнение неизвестного количества последовательных асинхронных вызовов будет грязным, независимо от того, что вы делаете. Особенно ужасно видеть все эти вложенные ключевые слова return
. Если мы newValues
массив newValues
через обещания в newValues
вместо того, чтобы сделать его глобальным для всей функции foo
, нам нужно будет настроить код так, чтобы он получал еще больше вложенных возвращаемых значений, например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
function foo() {
return getValues().then(function(values) {
return values.reduce(function(previousOperation, value) {
return previousOperation.then(function(newValues) {
return asyncOperation(value).then(function(newValue) {
console.log(newValue);
newValues.push(newValue);
return newValues;
});
});
}, Promise.resolve([]));
}).catch(function(err) {
console.log(‘We had an ‘, err);
});
}
|
Разве вы не согласны, что мы должны это исправить? Давайте посмотрим на решение.
Асинхронные функции на помощь
Даже с обещаниями асинхронное программирование не совсем простое и не всегда хорошо перетекает от А до Я. Синхронное программирование намного проще и написано и прочитано намного естественнее. Спецификация асинхронных функций рассматривает способы (с использованием генераторов ES6 за кулисами) написания вашего кода, как если бы он был синхронным.
Как мы их используем?
Первое, что нам нужно сделать, это поставить перед нашими функциями ключевое слово async
. Без этого ключевого слова мы не сможем использовать ключевое слово await
внутри этой функции, которое я объясню чуть позже.
Ключевое слово async
не только позволяет нам использовать await
, но и гарантирует, что функция вернет объект Promise
. Внутри асинхронной функции каждый раз, когда вы return
значение, функция фактически возвращает Promise
который разрешается с этим значением. Способ отклонения заключается в выдаче ошибки, в этом случае значением отклонения будет объект ошибки. Вот простой пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
async function foo() {
if( Math.round(Math.random()) )
return ‘Success!’;
else
throw ‘Failure!’;
}
// Is equivalent to…
function foo() {
if( Math.round(Math.random()) )
return Promise.resolve(‘Success!’);
else
return Promise.reject(‘Failure!’);
}
|
Мы даже не подошли к лучшей части, и мы уже сделали наш код больше похожим на синхронный код, потому что мы смогли прекратить явно возиться с объектом Promise
. Мы можем взять любую функцию и заставить ее возвращать объект Promise
просто добавив ключевое слово async
в начало.
Давайте продолжим и преобразуем наши функции getValues
и asyncOperation
:
1
2
3
4
5
6
7
|
async function getValues() {
return [1,2,3,4];
}
async function asyncOperation(value) {
return value + 1;
}
|
Легко! Теперь давайте посмотрим на лучшую часть всего: ключевое слово await
. В вашей асинхронной функции каждый раз, когда вы выполняете операцию, которая возвращает обещание, вы можете бросить перед ним ключевое слово await
, и оно прекратит выполнение остальной функции, пока возвращенное обещание не будет разрешено или отклонено. В этот момент await promisingOperation()
оценивается как разрешенное или отклоненное значение. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
function promisingOperation() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if( Math.round(Math.random()) )
resolve(‘Success!’);
else
reject(‘Failure!’);
}, 1000);
}
}
async function foo() {
var message = await promisingOperation();
console.log(message);
}
|
Когда вы вызываете foo
, он либо подождет, пока promisingOperation
разрешится, а затем выйдет из системы «Успех!» message или promisingOperation
будет отклонено, и в этом случае отклонение будет передано, а foo
отклонит его с сообщением «Failure!». Так как foo
ничего не возвращает, он разрешится с undefined
предположением, что promisingOperation
успешно выполняется.
Остается только один вопрос: как мы решаем ошибки? Ответ на этот вопрос прост: все, что нам нужно сделать, это обернуть его в блок try...catch
. Если одна из асинхронных операций будет отклонена, мы можем ее catch
и обработать:
1
2
3
4
5
6
7
8
|
async function foo() {
try {
var message = await promisingOperation();
console.log(message);
} catch (e) {
console.log(‘We failed:’, e);
}
}
|
Теперь, когда мы ознакомились со всеми основами, давайте рассмотрим наши предыдущие примеры обещаний и преобразуем их для использования асинхронных функций.
Примеры
Первый пример выше создал getValues
и использовал его. Мы уже заново создали getValues
поэтому нам просто нужно заново создать код для его использования. Здесь есть одно потенциальное предостережение для асинхронных функций: код должен быть в функции. Предыдущий пример был в глобальной области видимости (насколько можно было судить), но нам нужно обернуть наш асинхронный код в асинхронную функцию, чтобы заставить его работать:
1
2
3
|
async function() {
console.log(await getValues());
}();
|
Даже с включением кода в функцию я утверждаю, что его легче читать и у него меньше байтов (если вы удалите комментарий). Наш следующий пример, если вы правильно помните, делает все параллельно. Это немного сложно, потому что у нас есть внутренняя функция, которая должна возвращать обещание. Если мы используем ключевое слово await
внутри внутренней функции, эта функция также должна иметь префикс async
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
async function foo() {
try {
var values = await getValues();
var newValues = values.map(async function(value) {
var newValue = await asyncOperation(value);
console.log(newValue);
return newValue;
});
return await* newValues;
} catch (err) {
console.log(‘We had an ‘, err);
}
}
|
Возможно, вы заметили звездочку, прикрепленную к последнему ключевому слову await
. Похоже, это все еще немного обсуждается, но похоже, что await*
сути автоматически Promise.all
выражение справа в Promise.all
. Однако сейчас инструмент, который мы рассмотрим позже, не поддерживает await*
, поэтому его следует преобразовать в await Promise.all(newValues);
как мы делаем в следующем примере.
Следующий пример asyncOperation
параллельные вызовы asyncOperation
, но затем объединяет их и последовательно выводит данные.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
async function foo() {
try {
var values = await getValues();
var newValues = await Promise.all(values.map(asyncOperation));
newValues.forEach(function(value) {
console.log(value);
});
return newValues;
} catch (err) {
console.log(‘We had an ‘, err);
}
}
|
Я люблю это. Это очень чисто. Если мы Promise.all
ключевые слова await
и async
, Promise.all
оболочку getValues
и asyncOperation
getValues
и asyncOperation
синхронными, тогда этот код будет работать точно так же, за исключением того, что он будет синхронным. По сути, это то, что мы стремимся достичь.
В нашем последнем примере, конечно, все будет работать последовательно. Асинхронные операции не выполняются, пока не завершится предыдущая.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
async function foo() {
try {
var values = await getValues();
return await values.reduce(async function(values, value) {
values = await values;
value = await asyncOperation(value);
console.log(value);
values.push(value);
return values;
}, []);
} catch (err) {
console.log(‘We had an ‘, err);
}
}
|
Еще раз, мы делаем внутреннюю функцию async
. В этом коде обнаружена интересная особенность. Я передал []
в качестве значения «памятки», чтобы reduce
, но затем я использовал await
на нем. Значение справа от await
не обязательно должно быть обещанием. Это может принять любое значение, и если это не обещание, оно не будет ждать его; это будет просто работать синхронно. Однако, конечно, после первого выполнения обратного вызова мы фактически будем работать с обещанием.
Этот пример почти такой же, как и первый, за исключением того, что мы используем вместо map
вместо map
чтобы мы могли await
предыдущей операции, а затем потому, что мы используем reduce
для построения массива (это не то, что обычно делается особенно если вы создаете массив того же размера, что и исходный массив), нам нужно создать массив в обратном вызове, чтобы reduce
.
Использование асинхронных функций сегодня
Теперь, когда вы получили представление о простоте и удивительности асинхронных функций, вы можете плакать, как я это делал в первый раз, когда увидел их. Я не плакал от радости (хотя я почти сделал); нет, я плакал, потому что ES7 не будет здесь, пока я не умру! По крайней мере, так я себя чувствовал . Тогда я узнал о Traceur ,
Traceur написан и поддерживается Google. Это транспортер, который преобразует код ES6 в ES5. Это не помогает! Ну, это не так, за исключением того, что они также реализовали поддержку асинхронных функций , Это все еще экспериментальная функция, которая означает, что вам нужно явно указать компилятору, что вы используете эту функцию, и что вам определенно необходимо тщательно протестировать свой код, чтобы убедиться, что с компиляцией нет проблем.
Использование такого компилятора, как Traceur, означает, что у вас будет немного раздутый, некрасивый код, отправляемый клиенту, а это не то, что вам нужно, но если вы используете исходные карты, это, по сути, устраняет большинство недостатков, связанных с разработкой. Вы будете читать, писать и отлаживать чистый код ES6 / 7, а не читать, писать и отлаживать запутанный код, который должен обходить ограничения языка.
Конечно, размер кода все равно будет больше, чем если бы вы написали от руки код ES5 (скорее всего), поэтому вам может потребоваться найти какой-то баланс между поддерживаемым кодом и кодом производительности, но это баланс, который вам часто нужен найти даже без использования транспилера.
Использование Traceur
Traceur — это утилита командной строки, которую можно установить через NPM:
1
|
npm install -g traceur
|
В целом, Traceur довольно прост в использовании, но некоторые параметры могут сбивать с толку и могут потребовать некоторых экспериментов. Вы можете увидеть список вариантов для более подробной информации. --experimental
нас действительно интересует, так это опция --experimental
.
Вам нужно использовать эту опцию, чтобы включить экспериментальные функции, как мы работаем с асинхронными функциями. Если у вас есть файл JavaScript (в main.js
случае main.js) с включенным кодом ES6 и асинхронными функциями, вы можете просто скомпилировать его так:
1
|
traceur main.js —experimental —out compiled.js
|
Вы также можете просто запустить код, пропустив --out compiled.js
. Вы не увидите много, если в коде нет операторов console.log
(или других выходов консоли), но, по крайней мере, вы можете проверить на наличие ошибок. Вы, вероятно, захотите запустить его в браузере. Если это так, вам нужно предпринять еще несколько шагов.
- Загрузите
traceur-runtime.js
. Есть много способов получить его, но один из самых простых — от NPM:npm install traceur-runtime
. Файл будет доступен какindex.js
в папке этого модуля. - В своем HTML-файле добавьте тег
script
чтобы включитьscript
Traceur Runtime. - Добавьте еще один тег
script
подscript
Traceur Runtime для добавления вcompiled.js
.
После этого ваш код должен быть запущен!
Автоматизация компиляции Traceur
Помимо простого использования инструмента командной строки Traceur, вы также можете автоматизировать компиляцию, чтобы вам не приходилось возвращаться к консоли и перезапускать компилятор. Grunt и Gulp , которые являются автоматическими исполнителями задач, имеют свои собственные плагины, которые можно использовать для автоматизации компиляции Traceur : grunt-traceur и gulp-traceur соответственно.
Каждый из этих исполнителей задач может быть настроен для наблюдения за вашей файловой системой и повторной компиляции кода, как только вы сохраните любые изменения в ваших файлах JavaScript. Чтобы узнать, как использовать Grunt или Gulp, ознакомьтесь с их документацией «Начало работы».
Вывод
Асинхронные функции ES7 предлагают разработчикам способ по-настоящему выйти из ада обратного вызова таким способом, который обещания никогда не смогут выполнить самостоятельно. Эта новая функция позволяет нам писать асинхронный код способом, очень похожим на наш синхронный код, и, несмотря на то, что ES6 все еще ожидает своего полного выпуска, мы уже можем использовать асинхронные функции сегодня посредством транспиляции. Чего же ты ждешь? Выйди и сделай свой код потрясающим!