Я постоянно слышу людей, ноющих об асинхронных коллбэках в JavaScript
. Держать в голове порядок исполнения в этом языке немного трудно (это тот случай, который называют «Callback Hell» или «The Pyramid of Doom»), если до этого ты имел дело с синхронным программированием. Моим обычным ответом было «тебе придется как-то с этим обходиться». В конце концов, ожидаем ли мы, что все языки программирования будут выглядеть и ощущаться одинаково? Конечно нет.
Все поменял недавний обзор черновика ECMAScript 6
, в котором описываются генераторы — возможность языка, которая целиком изменит наш способ написания и серверного, и клиентского JavaScript
. С помощью генераторов мы можем превратить вложенные коллбэки в похожий на синхронный код без блокирования нашей единственной event loop
.
Например, этот код:
setTimeout(function(){
_get("/something.ajax?greeting", function(err, greeting) {
if (err) { console.log(err); throw err; }
_get("/else.ajax?who&greeting="+greeting, function(err, who) {
if (err) { console.log(err); throw err; }
console.log(greeting+" "+who);
});
});
}, 1000);
может быть написан так:
sync(function* (resume) {
try (e) {
yield setTimeout(resume, 1000);
var greeting = yield _get('/something.ajax?greeting', resume)
var who = yield _get('/else.ajax?who&greeting=' + greeting, resume)
console.log(greeting + ' ' + who)
}
catch (e) {
console.log(e);
throw e;
}
});
Интересно, не правда ли? Централизованная обработка исключений и понятный порядок исполнения.
Э-э, ECMAScript 6?
Примеры в этой статье будут работать в Chrome Canary 33.0.1716.0
. Примеры, за исключением тех, где есть XHR
, должны работать в Node.js
с флагом --harmony
(с версии 0.11, прим. перев.). Реализация генераторов, предлагаемая в JavaScript 1.7+
, не придерживается черновика ECMAScript 6
— так что вам придется внести некоторые изменения, чтобы заставить примеры работать в Firefox
. Если вы хотите запустить эти примеры в Canary
, вы можете запускать их в таком же виде, как здесь.
ES6 генераторы
Для того, чтобы понять, что происходит в примерах выше, мы должны поговорить о том, что такое ES6
генераторы и что они позволяют вам делать.
В соответствии с черновиком ECMAScript 6
, генераторы — это «сопрограммы первого класса, представляющие из себя объекты, инкапсулирующие отложенные контексты исполнения». Проще говоря, генераторы — это функции, которые могут останавливать свое исполнение (с помощью ключевого слова yield
) и продолжать свое исполнение с того же места после вызова их метода next
. JavaScript
все так же выполняет только одну задачу в одно и то же время, но он теперь в состоянии приостанавливать выполнение в середине тела функции-генератора и переключать контекст на исполнение чего-то другого. Генераторы не дают возможности параллельного исполнения кода и они не умеют обращаться с потоками.
Скромный итератор
Теперь, когда мы немного разобрались, давайте посмотрим на код. Мы напишем небольшой итератор, чтобы продемонстрировать синтаксис остановки/продолжения.
function* fibonacci() {
var a = 0, b = 1, c = 0;
while (true) {
yield a;
c = a;
a = b;
b = c + b;
}
}
function run() {
var seq = fibonacci();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1
console.log(seq.next().value); // 1
console.log(seq.next().value); // 2
console.log(seq.next().value); // 3
console.log(seq.next().value); // 5
}
run();
Что здесь происходит:
- Функция
run
инициализирует генератор чисел Фибоначчи (он описан специальным синтаксисомfunсtion*
). В отличие от обычной функции этот вызов не начинает исполнение ее тела, а возвращает новый объект — генератор. - Когда функция
run
вызывает метод генератораnext
(синхронная операция), код выполняется до того момента, пока не встретит операторyield
. - Выполнение оператора
yield
останавливает генератор и возвращает результат наружу. Операции, следующие заyield
в этот момент не были выполнены. Значение (операндa
заyield
) будет доступно снаружи через свойствоvalue
у результата исполнения.
При следующем вызове методаnext
у генератора, выполнение кода продолжается с того места, где оно остановилось на предыдущемyield
.
Вам, наверно, интересно, выйдет ли генератор когда-либо из цикла while
. Нет, он будет исполняться внутри цикла до тех пор, пока кто-то вызывает его метод next
.
Следим за исполнением кода
Как было показано в предыдущем примере, код, расположенный в теле генератора после yield
, не будет исполнен до тех пор, пока генератор не будет продолжен. В генератор так же можно передать аргумент, который будет подставлен вместо того yield
, на котором было прервано предыдущее исполнение генератора.
function* powGenerator() {
var result = Math.pow(yield "a", yield "b");
return result;
}
var g = powGenerator();
console.log(g.next().value); // "a", from the first yield
console.log(g.next(10).value); // "b", from the second
console.log(g.next(2).value); // 100, the result
Первое выполнение генератора возвращает значение "a"
в качестве свойства value
результата исполнения. Затем мы продолжаем исполнение, передав в генератор значение 10
. Воспользуемся подстановкой, чтобы продемонстрировать, что происходит:
function* powGenerator() {
var result = Math.pow(----10----, yield "b");
return result;
}
Затем генератор доходит до второго yield
и снова приостанавливает свое исполнение. Значение "b"
будет доступно в возвращенном объекте. Наконец, мы снова продолжаем исполнение, передавая в качестве аргумента 2
. Снова подстановка:
function* powGenerator() {
var result = Math.pow(----10----, ----2----);
return result;
}
После этого вызывается метод pow
, и генератор возвращает значение, хранимое в переменной result
.
Ненастоящая синхронность: блокирующий Ajax
Итератор, выдающий последовательность Фибоначчи, и математические функции со множеством точек входа интересны, но я обещал показать вам способ избавиться от коллбэков в вашем JavaScript
коде. Как выясняется, мы можем взять некоторые идеи из предыдущих примеров.
Прежде чем мы посмотрим на следующий пример, обратите внимание на функцию sync
. Она создает генератор, передавая ему функцию resume
и вызывает метод next
на нем, чтобы запустить его исполнение. Когда генератору необходим асинхронный вызов, он использует resume
как коллбэк и выполняет yield
. Когда асинхронный вызов выполняет resume
, он вызывает метод next
, продолжая исполнение генератора и передавая в него результат работы асинхронного вызова.
Обратно к коду:
// **************
// framework code
function sync(gen) {
var iterable, resume;
resume = function(err, retVal) {
if (err) iterable.raise(err);
iterable.next(retVal); // resume!
};
iterable = gen(resume);
iterable.next();
}
function _get(url, callback) {
var x = new XMLHttpRequest();
x.onreadystatechange = function() {
if (x.readyState == 4) {
callback(null, x.responseText);
}
};
x.open("GET", url);
x.send();
}
// ****************
// application code
sync(function* (resume) {
log('foo');
var resp = yield _get("blix.txt", resume); // suspend!
log(resp);
});
log('bar'); // not part of our generator function’s body
Можете ли вы догадаться, что вы увидете в консоли? Правильный ответ: «foo», «bar» и «то, что находится в blix.txt». Располагая код внутри генератора, мы делаем его похожим на обычный синхронный код. Мы не блокируем поток event loop
; мы останавливаем генератор и продолжаем выполнять код, расположенный дальше после вызова next
. Будущий коллбэк, который будет вызван на другом тике, продолжит наш генератор, передав ему нужное значение.
Централизованная обработка ошибок
Централизованная обработка ошибок внутри нескольких асинхронных коллбэках — это боль. Вот пример:
try {
firstAsync(function(err, a) {
if (err) { console.log(err); throw err; }
secondAsync(function(err, b) {
if (err) { console.log(err); throw err; }
thirdAsync(function(err, c) {
if (err) { console.log(err); throw err; }
callback(a, b, c);
});
});
});
}
catch (e) {
console.log(e);
}
Блок catch
никогда не будет исполнен из-зи того, что выполнение коллбэка — это часть совершенно другого коллстека, в другом тике event loop
. Обработка исключений должна быть расположена внутри самой функции-коллбэка. Можно реализовать функцию высшего порядка, чтобы избавиться от некоторых повторяющихся проверок на наличие ошибок и убрать некоторые вложения с помощью библиотеки навроде async
. Если же следовать соглашению Node.js
об ошибке, как о первом аргументе, можно написать общий обрабочик, который будет возвращать все ошибки назад в генератор:
function sync(gen) {
var iterable, resume;
resume = function(err, retVal) {
if (err) iterable.raise(err); // raise!
iterable.next(retVal);
};
iterable = gen(resume);
iterable.next();
}
sync(function* (resume) {
try {
var x = firstAsync(resume);
var y = secondAsync(resume);
var z = thirdAsync(resume);
// … do something with your data
}
catch (e) {
console.log(e); // will catch errors from any of the three calls
}
});
Теперь исключения, которые возникнут внутри любой из трех функций будут обработаны единственным блоком catch
. А исключение, возникшее в любой из трех функций, не даст последующим функциям исполниться. Очень хорошо.
Одновременные операции.
То, что код генератора исполняется сверху вниз, не значит, что вы не можете работать с несколькими асинхронными операциями одновременно. Бибилиотеки навроде genny
и gen-run
дают такой API: они просто выполняют некоторое количество асинхронных операций перед тем, как продолжить исполнение генератора. Пример, с использование genny
:
genny.run(function* (resume) {
_get("test1.txt", resume());
_get("test2.txt", resume());
var res1 = yield resume, res2 = yield resume; // step 1
var res3 = yield _get("test3.txt", resume()); // step 2
console.log(res1 + res2);
});
Итого
Асинхронные коллбэки де-факто были основным паттерном в JavaScript
на протяжении долго времени. Но теперь вместе с генераторами в браузере (Firefox
с JavaScript 1.7
и Chrome Canary
несколько месяцев назад) все меняется. Новые конструкции управления исполнением дают возможность использовать совершенно новый стиль программирования, такой, который сможет соперничать с традиционным стилем вложенных коллбэков. Осталось дождаться, когда стандарт ECMAScript 6
будет реализован в завтрашних движках JavaScript
.
Автор: Verkholantsev