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
.
ссылка на оригинал статьи http://habrahabr.ru/post/210330/
Добавить комментарий