Эволюция асинхронного JavaScript

Функции async уже практически здесь — но дорога к ним была довольно долгой. Не так давно мы писали коллбэки, потом появились Promise / A+ спецификации, следом за ними — функции-генераторы, и теперь async функции.

Давайте взглянем назад и посмотрим, как асинхронный JavaScript развивался с годами. 

Коллбэки

Все началось с коллбэков.

Асинхронный JavaScript

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

Something.save(function(err) {
    if (err) {
    //обработка ошибок
       return;
    }
    console.log('success');
});

Эти так называемые error-first коллбэки лежат в сердце Node.js — модули ядра используют их так же, как и большинство модулей, которые вы можете найти в NPM.

Проблемы с коллбэками:

  • легко написать “callback hell” или спагетти-код, если не использовать их должным образом;
  • легко упустить обработку ошибок;
  • нельзя возвращать значения с выражением return, как и нельзя использовать ключевое слово throw.

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

Одним из ответов был модуль async. Если вы много работали с коллбэками, то знаете, как сложно может быть заставить что-то, работающее параллельно, работать последовательно; даже маппинг массивов использует асинхронные функции. Тогда благодаря Каолану Макмейхону (Caolan McMahon) родился модуль async.

С async вы можете просто делать так:

async.map([1, 2, 3], AsyncSquaringLibrary.square,
          function(err, result){
          // результатом будет [1, 4, 9]
          });

Promises

Текущая спецификация JS Promises (в переводе с английского — обещания) берет начало в 2012 и доступна с ES6 — однако обещания не были разработаны сообществом JavaScript. Термин придумал Дэниел Фридман в 1976 году.

Promise представляет собой конечный результат асинхронной операции.

Предыдущий пример с использование обещаний выглядел бы так:

Something.save()
    .then(function() {
        console.log('success');
    })
    .catch(function() {
        //обработка ошибки
    })

Вы можете заметить, что обещания также используют коллбэки. Как then, так и catch регистрируют коллбэки, которые будут вызваны как при успешном завершении асинхронной операции, так и если она по каким-то причинам не будет выполнена. Другой плюс Promises в том, что они могут быть связаны цепочкой:

saveSomething()
    .then(updateOtherthing)
    .then(deleteStuff)
    .then(logResults);

При использовании обещаний вы можете использовать полифиллы в средах выполнения, в которых их еще нет. Популярный выбор в таких случаях — bluebird. Эти библиотеки могут предоставлять гораздо большую функциональность, чем нативные обещания — но даже в этих случаях следуйте спецификациям Promises/A+.

Вы можете спросить: как я могу использовать обещания, когда большинство библиотек предоставляют только коллбэк-интерфейсы?

Хорошо, это довольно-таки легко: единственная вещь, которую вы должны сделать — это обернуть коллбэк в функцию, возвращающую обещание:

function saveToTheDb(value) {
    return new Promise(function(resolve, reject) {
        db.values.insert(value, function(err, user) { // ошибка идет первой
            if (err) {
                return reject(err); // не забудьте вернуть здесь
            }
            resolve(user);
        })
    }
}

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

function foo(cb) {
    if (cb) {
        return cb();
    }
    return new Promise(function (resolve, reject) {

    });
}

Или даже проще, вы можете можете начать с интерфейсов обещаний и предоставить совместимость с такими инструментами, как callbackify. Callbackify в основном делает те же вещи, что и предыдущий фрагмент кода, но более общим путем.

Генераторы / yield

Генераторы JavaScript — это относительно новый концепт, который был представлен в ES6.

Разве не было бы здорово, если бы при выполнении функции вы могли бы приостанавливать её в какой-либо точке, рассчитывать что-то, делать другие вещи, а затем возвращаться к ней, возможно, с каким-то значением, и продолжать её выполнение?

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

function* foo () {
    var index = 0;
    while (index < 2) {
        yield index++;
    }
}
var bar = foo();

console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }

Если вы хотите легко использовать генераторы для написания асинхронного JavaScript, вам также понадобится пакет co.

С co наши предыдущие примеры могли бы выглядеть так:

co(function* (){
    yield Something.save();
}).then(function() {
    // success
})
.catch(function(err) {
    //error handling
});

Вы можете спросить: а что с операциями, выполняющимися параллельно? Ответ проще, чем вы могли бы подумать (под этой оберткой всего лишь Promise.all):

yield [Something.save(), Otherthing.save()];

Async / await

Async-функции были представлены в ES7, и в настоящее время доступны только с транспайлерами, такими как babel (сейчас вы говорим о ключевом слове async, а не о пакете async).

В двух словах, с ключевым словом async мы можем делать то, что мы делали с комбинацией co и генераторов — кроме хаков.

denicola-yield-await-asynchronous-javascript

Под капотом у async функции обещания — поэтому async-функция возвращает тип Promise.

Поэтому если мы хотим сделать то же самое, что и предыдущих примерах, мы должны переписать наш код следующим образом:

async function save(Something) {
    try {
        await Something.save()
    } catch (ex) {
        //error handling
    }
    console.log('success');
}

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

Параллелизм async-функций довольно-таки схож с yield-подходом – за исключением того, что Promise.all не скрыта, но вы должны вызвать её:

async function save(Something) {
    await Promise.all[Something.save(), Otherthing.save()]
}

Koa уже поддерживает async-функции, так что вы можете опробовать их сегодня, используя babel.

import koa from koa;
let app = koa();

app.experimental = true;

app.use(async function (){
    this.body = await Promise.resolve('Hello Reader!')
})

app.listen(3000);

За перевод благодарим нашего подписчика, Александра Хисматулина

Перевод статьи «The Evolution of Asynchronous JavaScript»