Руководство по асинхронным функциям ES7
JavaScript

Руководство по асинхронным функциям ES7

Оригинал: A Primer on ES7 Async Functions, Joe Zimmerman

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

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

В ECMAScript 7 это станет больше, чем просто мечтой: это станет реальностью. Я покажу вам эту реальность — называемую асинхронными функциями — прямо сейчас. Почему мы говорим об этом сейчас? Ведь даже ES6 не был полностью завершен (спецификация была одобрена в июне 2015 года, после выхода статьи — прим. переводчика), так что кто знает, сколько времени пройдет, прежде чем мы увидим ES7. Правда, вы можете использовать эту технологию уже сейчас, и в конце этой статьи я покажу вам как.

Текущее положение дел

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

Примеры

В качестве первого примера рассмотрим очень простой код: вызов асинхронной функции и вывод в консоль ее результата.


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

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

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


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);
    });
}

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


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);
    });
}

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


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 в обещание вместо того, чтобы сделать его глобальным внутри функции foo(), мы получим еще больше вложенных выражений return.


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. Какое бы значение вы не вернули из асинхронной функции, она всегда фактически вернет объект Promise с этим значением. Чтобы обещание выполнилось неуспешно, нужно бросить исключение. Например:


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. Мы взяли функцию и заставили ее возвращать обещание, просто добавив ключевое слово async.

Давайте преобразуем функции getValues и asyncOperation:


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

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


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 и выводит либо «Success!», если обещание успешно, либо «Failure!», если неуспешно.

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


async function foo() {
    try {
        var message = await promisingOperation();
        console.log(message);
    } catch (e) {
        console.log('We failed:', e);
    }
}

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

Примеры

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


async function() {
    console.log(await getValues());
}(); // Скобки "()" вызывают функцию немедленно

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


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. Однако инструмент, о котором мы поговорим позже, не поддердивает await*, поэтому можно использовать await Promise.all(newValues), как мы и сделаем в следующем примере.

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


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);
    }
}

Мне это нравится. Это чистый код. Если убрать все await и async, удалить обертку Promise.all и сделать функции getValues и asyncOperation синхронными, этот код будет работать в точности так же, как если бы он был синхронным. Именно этого, по существу, мы и стремимся достичь.

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


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);
    }
}

Снова мы делаем внутреннюю функцию асинхронной. Есть одна интересная особенность, которая проявляется в этом коде. Я передал пустой массив в качестве знаения «memo» в функцию reduce, но потом я использовал await c этим значением. Значение, которое используется с await, не обязательно должно быть массивом. Оно может принимать любые значения, но если это не объект Promise, код просто выполняется синхронно. И конечно, после первого прохода, мы уже будем работать с обещанием.

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

Использование асинхронных функций сегодня

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

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

Использование транслятора, такого как Traceur, означает, что вы будете отдавать клиенту немного раздутый и уродливый код. Но работать вы будете с чистым ES6/7 кодом, вместо запутаного.

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

Использование Traceur

Traceur — это утилита командной строки, которая может быть установлена через NPM:


npm install -g traceur

В основном, Traceur очень прост в использовании, но некоторые опции требуют экспериментов. Нас будет интересовать опция —experimental.

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


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

Вы также можете просто запустить команду выше, опустив —out compiled.js. Вы не увидите ничего, если код не содержит вызовов console.log (или других методов вывода в консоль), но, по крайней мере, вы можете проверить код на наличие ошибок. Чтобы запустить код в браузере, необходимо предпринять несколько шагов:

  • Скачать traceur-runtime.js. Проще всего сделать это через npm: npm install traceur-runtime. Скрипт будет доступен как index.js в папке модуля.
  • Подключить скрипт Traceur Runtime в вашем HTML-файле.
  • Подключить скрипт compiled.js.

После этого код можно запустить!

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

Запускать компиляцию Traceur можно не только вручную из командной строки, но и с помощью таких инструментов, как Grunt или Gulp. Каждый из их этих сборщиков можно настроить таким образом, чтобы он следил за изменениями файлов в проекте и запускал перекомпиляцию после кадого изменения. Подробнее о каждом сборщике можно прочитать в соответствующей документации (про gulp есть отличная статья на нашем сайте — прим. переводчика).

Заключение

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

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться