Статьи

Учебник по асинхронным функциям ES7

Если вы следили за миром 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 — это утилита командной строки, которую можно установить через 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 (или других выходов консоли), но, по крайней мере, вы можете проверить на наличие ошибок. Вы, вероятно, захотите запустить его в браузере. Если это так, вам нужно предпринять еще несколько шагов.

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

После этого ваш код должен быть запущен!

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

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

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