Advertisement
  1. Code
  2. JavaScript

Знакомство с ES7 Async функциями

Scroll to top
Read Time: 12 min

() translation by (you can also view the original English article)

В случае если вам довелось следить за событиями в мире JavaScript, вероятно вы знакомы с промисами (promises). Есть множество отличных туториалов онлайн, если есть необходимость узнать больше о промисах, я не буду объяснять их здесь, данная статья предполагает, что у вас есть рабочий опыт применения промисов.

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

В ECMAScript7, это будет не просто мечта: всё это станет для нас реальностью, и я хочу познакомить вас с реальность - с функций async, прямо сейчас. Зачем мы об этом говорим? На данный момент ES6 даже не полностью утверждён, кто знает, сколько ещё придётся ждать, прежде чем мы увидим ES7. Правда в том, что уже сейчас можно использовать данную технологию и в конце этого поста я расскажу вам как.

Нынешнее положение дел

До того как я начну демонстрировать, как использовать функции async, я хочу показать несколько примеров с промисами (используя промисы ES6). Позже, я разберу данные примеры с использованием функций aysnc, чтобы вы могли понять разницу.

Примеры

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

1
function getValues() {
2
    return Promise.resolve([1,2,3,4]);
3
}
4
5
getValues().then(function(values) {
6
    console.log(values);
7
});

Теперь когда мы взглянули на простой пример, давайте займёмся чем-нибудь более сложным. Я буду использовать и модифицировать примеры из поста на моём личном блога, там можно найти описание некоторых паттернов связанных с использованием промисов, в различных ситуациях. Каждый из примеров асинхронно обрабатывает массив значений, выполняет асинхронные действия, которые изменяют каждое значение в массиве, логирует новые значения, и наконец возвращает массив с новыми значениями.

Сначала мы посмотрим на пример, параллельно запускающий множество асинхронных действий, после незамедлительно реагирует на них, после того, как каждая из них завершается, в не зависимости от порядка окончания работы функций. Функция getValues - точно такая же как в предыдущем примере.  Функция asyncOperation, также будет использована в примерах, которые мы рассмотрим позднее.

1
function asyncOperation(value) {
2
    return Promise.resolve(value + 1);
3
}
4
5
function foo() {
6
    return getValues().then(function(values) {
7
        var operations = values.map(function(value) {
8
            return asyncOperation(value).then(function(newValue) {
9
                console.log(newValue);
10
                return newValue;
11
            });
12
        });
13
 
14
        return Promise.all(operations);
15
    }).catch(function(err) {
16
        console.log('We had an ', err);
17
    });
18
}

Можно сделать тоже самое, но стоит убедиться, что логирование идёт в таком же порядке, в каком находятся элементы в массиве. Другими словами, следующий пример выполнит асинхронную работу параллельно, однако все синхронные операции будут последовательными:

1
function foo() {
2
    return getValues().then(function(values) {
3
        var operations = values.map(asyncOperation);
4
       
5
        return Promise.all(operations).then(function(newValues) {
6
            newValues.forEach(function(newValue) {
7
                console.log(newValue);
8
            });
9
 
10
            return newValues;
11
        });
12
    }).catch(function(err) {
13
        console.log('We had an ', err);
14
    });
15
}

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

1
function foo() {
2
    var newValues = [];
3
    return getValues().then(function(values) {
4
        return values.reduce(function(previousOperation, value) {
5
            return previousOperation.then(function() {
6
                return asyncOperation(value);
7
            }).then(function(newValue) {
8
                console.log(newValue);
9
                newValues.push(newValue);
10
            });
11
        }, Promise.resolve()).then(function() {
12
        return newValues;
13
        });
14
    }).catch(function(err) {
15
        console.log('We had an ', err);
16
    });
17
}

Даже промисы не помогают уменьшить количество вложенных вызовов. Запуская неизвестное количество последовательных асинхронных вызовов, в итоге приведёт к беспорядку, что бы мы не делали. Особенно это можно увидеть, взглянув на огромное количество вложенных return. Если передать массив newValues промисам в коллбеке (callback) reduce, вместо того, чтобы делать его глобальным для всей функции foo, придётся переписать код, таким образом, что в нём будет ещё больше return, выглядит это так:

1
function foo() {
2
    return getValues().then(function(values) {
3
        return values.reduce(function(previousOperation, value) {
4
            return previousOperation.then(function(newValues) {
5
                return asyncOperation(value).then(function(newValue) {
6
                    console.log(newValue);
7
                    newValues.push(newValue);
8
                    return newValues;
9
                });
10
            });
11
        }, Promise.resolve([]));
12
    }).catch(function(err) {
13
        console.log('We had an ', err);
14
    });
15
}

Не кажется ли вам, что следует это исправить? Давайте разберём решение данной проблемы.

Функция Async приходит на помощь

Даже имея под рукой промисы, асинхронное программирование не становится таким простым занятием, как хотелось бы. Синхронное программирование гораздо проще, код выглядит более понятным и его привычно читать. Спецификация функций Async, пытается найти средство (используя генераторы ES6 под капотом) для написания кода, который похож на синхронный.

Как их использовать?

Первое что следует сделать, так это добавить ключевое слов async перед функцией. Без ключевого слова, мы не сможем применять очень важное ключевое слово await, внутри этой функции, скоро я дам объяснение, касательно этого.

Ключевое слово async не только позволяет использовать await, помимо этого функцией будет возвращён объект Promise. Внутри асинхронной функции, каждый раз когда возвращается значение, функция вернёт Promise, который будет ассоциирован с этим значением. Также есть возможность вернуть ошибку, в этом случае возвращаемое значение будет представлять из себя объект ошибки. Вот простой пример:

1
async function foo() {
2
    if( Math.round(Math.random()) )
3
        return 'Success!';
4
    else
5
        throw 'Failure!';
6
}
7
8
// Is equivalent to...

9
10
function foo() {
11
    if( Math.round(Math.random()) )
12
        return Promise.resolve('Success!');
13
    else
14
        return Promise.reject('Failure!');
15
}

Мы ещё даже не добрались до самого интересного и уже сделали наш код, более похожим на синхронный, так как нам удалось не взаимодействовать на прямую с объектом Promise. Можно взять любую функцию и она будет возвращать объект Promise, лишь добавив перед ней ключевое слово async.

Пойдём дальше, сконвертируем getValues и функцию asyncOperation:

1
async function getValues() {
2
    return [1,2,3,4];
3
}
4
5
async function asyncOperation(value) {
6
    return value + 1;
7
}

Очень просто! Теперь разберёмся с самым интересным: ключевым словом await. Внутри асинхронной функции, каждый раз, когда выполняются действия возвращающие промис, вы можете добавить ключевое слово await, перед ним, тем самым функция не будет выполняться до того момента, как возвращаемый промис будет обработан, либо отклонён. На этом этапе, await promisingOperation() будет возвращать объект промис, либо объект с ошибкой. К примеру:

1
function promisingOperation() {
2
    return new Promise(function(resolve, reject) {
3
        setTimeout(function() {
4
            if( Math.round(Math.random()) )
5
                resolve('Success!');
6
            else
7
                reject('Failure!');
8
        }, 1000);
9
    }
10
}
11
12
async function foo() {
13
    var message = await promisingOperation();
14
    console.log(message);
15
}

Когда мы вызываем foo, она будет ждать результата promisingOperation, после чего будет выведен "Success!-" или promisingOperation вернёт ошибку, в таком случае foo сообщит об этом "Failure!". Так как foo ничего не возвращает, в результате мы получим undefined, предполагая , что promisingOperation успешно завершилась.

Остаётся лишь один вопрос, как нам стоит поступать с ошибками? Ответ на этот вопрос очень прост: всё что нам нужно сделать, так это обернуть блок в try...catch. Если одна из асинхронных операций, будет возвращать ошибку, мы можем обработать её с помощью catch:

1
async function foo() {
2
    try {
3
        var message = await promisingOperation();
4
        console.log(message);
5
    } catch (e) {
6
        console.log('We failed:', e);
7
    }
8
}

Теперь когда мы разобрались с основами, давайте вернёмся к предыдущему примеру и добавим функциями async.

Примеры

Первый пример выше создал getValues и мы его используем. Мы уже пересоздали getValues, поэтому остаётся лишь переписать код, чтобы можно было его применять. Есть одна оговорка, касательно async функций, которую мы можем наблюдать здесь: код должен находиться внутри функции. Предыдущий пример был реализован в глобальной области видимости (вы наверно уже это заметили), но нам следует обернуть код async в async функцию, чтобы он работал:

1
async function() {
2
    console.log(await getValues());
3
}(); // The extra "()" runs the function immediately

Даже после того как мы обернули код в функцию, я всё  ещё утверждаю, что читается это гораздо понятнее и занимает меньше места (если убрать комментарий). Наш следующий пример, если вы помните, делает всё параллельно. Ситуация здесь не совсем очевидная, так как у нас есть внутренняя функция, которая должна вернуть промис. Если мы используем ключевое слово await, во внутренней функции, перед ней также должен идти async.

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
5
        var newValues = values.map(async function(value) {
6
            var newValue = await asyncOperation(value);
7
            console.log(newValue);
8
            return newValue;
9
        });
10
       
11
        return await* newValues;
12
    } catch (err) {
13
        console.log('We had an ', err);
14
    }
15
}

Вероятно вы заметили звёздочку перед последним ключевым словом await. По поводу этого ещё идут некоторые споры, тем не менее кажется, что await* в итоге автоматически будет оборачивать выражение в Promise.all. Однако на данный момент, инструмент, который мы разберём немного позже, не поддерживает await*, тем самым следует сконвертировать данный синтаксис в Promise.all(newValues); это видно в следующем примере.

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

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
        var newValues = await Promise.all(values.map(asyncOperation));
5
6
        newValues.forEach(function(value) {
7
            console.log(value);
8
        });
9
10
        return newValues;
11
    } catch (err) {
12
        console.log('We had an ', err);
13
    }
14
}

Мне это правда нравится. Код выглядит чисто. Если убрать ключевые слова await и async, убрать обёртку Promise.all, затем сделать getValues и asyncOperation синхронными, скрипт будет работать точно также, только всё будет синхронно. Такого результат в конце концов мы и пытаемся добиться.

Последний пример, само собой, в нём всё будет работать синхронно. Не будет выполнено каких-либо асинхронных операций, до момента, пока следующая не завершиться.

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
5
        return await values.reduce(async function(values, value) {
6
            values = await values;
7
            value = await asyncOperation(value);
8
            console.log(value);
9
            values.push(value);
10
            return values;
11
        }, []);
12
    } catch (err) {
13
        console.log('We had an ', err);
14
    }
15
}

Опять же, мы делаем внутреннюю async функцию. В данном коде встречается интересная странность. Я передал reduce - [], в который будут записываться значения, однако затем использовал await. Значение справа от await, не обязательно должно быть промисом. Функция может принимать любые значения, в том случае если это не промис, функция не будет дожидаться, а запустится асинхронно. Тем не менее, после первого запуска коллбека, нам предстоит работать с промисом.

Данный пример выглядит схоже с первым примером, за исключением того, что мы используем reduce вместо map, таким образом мы можем дождаться await предыдущей операции, после чего, так как мы используем reduce для формирования массива (не совсем обычное явление, особенно если создаём массив такого же размера, как и оригинальный), нам необходимо создать массив внутри коллбека для reduce.

Асинхронные функции сегодня

Теперь, когда вам довелось взглянуть на простоту и эффективность asycn функций, возможно вам захотелось заплакать, такое же чувство первый раз посетило меня, когда я их увидел. Я не плакал от радости (может быть и плакал); нет, я плакал по причине, что ES7 не появится до тех пор пока я жив! По крайней мере, так я себя чувствовал. Потом я узнал о Traceur.

Traceur написан и поддерживается Google. Это транспайлер, который конвертирует код ES6 в ES5. Но это не поможет! Возможно, тем не менее, они также реализовали поддержку async функций. По сей день это является экспериментальной особенностью, это значит, что вам нужно явно сообщить компилятору, что данная особенность понадобится и вы будете её использовать, и что вы подробно протестируете код, убедившись, что с компиляцией не будет проблем.

Используя компилятор на подобии Traceur означает - клиенту придётся отправить немного громоздкий, некрасивый код, чего бы совсем не хотелось, но если прибегнуть к помощи source maps, вы сможете избежать большинства недостатков связанных с разработкой. Вы будете читать, писать и отлаживать чистый ES6/7 код, вместо чтения, отладки и написания беспорядочного кода, который пытается обойти ограничения языка.

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

Используя Traceur

Traceur - утлита для командной строки, которую можно установить с помощью NPM:

1
npm install -g traceur

В основном, Traceur довольно прост в использовании, но некоторые опции, могут запутать и потребуют некоторых экспериментов. Ознакомьтесь со списком опций. Больше всего нас интересует опция --experimental.

Эту опцию нужно использовать для включения экспериментальных особенностей, которые понадобятся для работы функций async. Как только у вас будет под рукой JavaScript файл (main.js в моём случае), содержащий код ES6 с async функциями, можно его скомпилировать следующим образом:

1
traceur main.js --experimental --out compiled.js

Также можно запустить код без --out compiled.js. Много вы не увидите, пока в коде нет console.log (или других опций для вывода в консоль), но по крайней мере, можно отладить ошибки. В любом случае скорее всего вы хотите запустить код в браузере. Если это ваш случай, придётся сделать ещё несколько дополнительных шагов.

  1. Скачайте скрипт traceur-runtime.js. Есть множество способов сделать это, самый простой с помощью NPM: npm install traceur-runtime. После чего файл будет доступен, как index.js в каталоге модулей.
  2. В вашем HTML файле, добавьте элемент script, который будет содержать исполняемый скрипт Traceur.
  3. Под скриптом Traceur, добавьте другой элемент script, содержащий compiled.js.

После этого код должен работать!

Автоматизация компиляции Traceur

Помимо применения Traceur в качестве инструмента командной строки, можно автоматизировать компиляцию, таким образом не придётся каждый раз возвращаться в консоль для повторного запуска компилятора. Grunt и Gulp, которые являются инструментами для автоматизации подобных задач, каждый из них имеет свои плагины, есть возможность всегда ими воспользоваться, они помогут в автоматизации компиляции Traceur: grunt-traceur и gulp-traceur.

Каждый из этих инструментов автоматизации может быть настроен таким образом - он будет отслеживать изменения в файлах и каждый раз компилировать код, как только будут сохранены изменения в JavaScript файлах. Ознакомиться с тем как использовать Grunt или Gulp, можно изучив документацию.

Заключение

Функции async из ES7 предоставляют разработчикам способ избавится от мешанины с коллбеками, так как не могут этого сделать промисы. Эта новая особенность позволяет писать асинхронный код, так что он будет максимально похож на синхронный, и даже не смотря на тот факт, что ES6 ещё не полностью доступен, мы уже можем воспользоваться функциями async, благодаря транспайлерам. Так чего же вы ждёте? Не упускайте возможность улучшить качество своего кода!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.