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

Что же такое генераторы и как они работают?


Генераторы — это функции которые можно запустить, приостановить и возобновить на различных стадиях выполнения. По сути дела, эта специальная функция возвращает итератор. Функции генераторы обозначаются звездочкой после ключевого слова function, а вся магия спрятана в использовании специального ключевого слова yield. Оно определяет, что итератор должен вернуть после очередного вызова метода next().

И сразу же пример простого генератора в ES6:

function* myGenerator() {
    yield ‘first’;
    let input = yield ‘second’;
    yield input;
}

// Получаем объект генератора
let iterator = myGenerator();

// Запускаем генератор, доходим до первого yield
console.log(iterator.next()): // { value: ‘first’, done: false }

// Возобновляем(не передаем никакого значения), доходим до второго yield
console.log(iterator.next()); // { value: ‘second’, done: false }

// Возобновляем (передаем значение) доходим до последнего yield
console.log(iterator.next(‘third’)); // { value: ‘third’, done: false }

// Заканчивается работа (yield больше нет)
console.log(iterator.next()); // { value: undefined, done: true }

Итак, что же здесь происходит?

  • Мы объявляем функцию генератор, используя специальный синтаксис function* myGenerator() {}.
  • Первый вызов этой функции возвращает объект итератора. У этого объекта есть метод next для возобновления функции генератора в его текущем состоянии.
  • Функция генератор не начинает своего выполнения до тех пор, пока мы не запустим iterator.next.
  • Каждый раз, при вызове iterator.next, функция возобновляет свое выполнение с места последней паузы и выполняет весь код, пока не «наткнется» на следующий yield и опять станет на паузу.
  • Вызов iterator.next возвращает объект, содержащий значение, которые было передано в yield и флаг, который обозначает закончила функция свое выполнение или нет.

Генераторы в роли итераторов


В начале статьи было упомянуто, что зачастую генераторы используются как простой механизм создания итераторов. Все это можно сделать используя синтаксис for of:

Например:

function* myGenerator() {
   yield ‘first’;
   yield ‘second’;
   yield ‘third’;
}
for (var v of myGenerator()) {
   console.log(v);
}

Или например:

function* myGenerator(start, stop) {
   for (var i = start; i < stop; i++)
      yield i;
}
for (var v of myGenerator()) {
 console.log(v);
}

Ладно, итераторы это здорово, но давайте посмотрим какие вещи покруче можно сделать используя генераторы.

Итак, нам нужно залогиниться на какой то бекенд, затем использовать токен аутентификации для того, чтобы извлекать данные из API. Если применить механизм генераторов это будет выглядеть так:

co(function* () {
   var result = yield login(username, password);
   var posts = yield getPosts(result.token);
   return posts;
}).then(value => {
   console.log(value);
}, err => {
   console.error(err);
});

function login(username, password) {
   return fetch(‘/login’, {
     method: ‘post’,
     body: JSON.stringify({
       username: username
       password: password
})
 }).then(response => response.json());
}
function getPosts(token) {
   return fetch(‘/posts’, {
     headers: new Headers({
       ‘X-Security-Token’: token
   })
 }).then(response => response.json());
}

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

Express vs Koa


Эээээ, Koa? Это что вообще такое?

Koa — это фреймворк нового поколения(из подзаголовка официального сайта), который улучшает механизм написания middleware, используя как раз таки генераторы и библиотеку co, и избавляя таким образом ваше приложения от ада из коллбеков.
С философской точки зрения, Koa стремится “исправить и заменить node”, а Express наоборот, “расширить node”(из документации Koa).


Middleware в Koa построены на механизме генераторов и представляют собой “каскад”. Проще говоря, сначала запускается механизм “вниз по течению”(downstream) через все middleware, а после этого “вверх по течению” через все middleware(upstream).

Итак, пример:

var koa = require('koa');
var app = koa();

// x-response-time

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  this.set('X-Response-Time', ms + 'ms');
});

// logger

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

Этот пример вернет нам 'Hello World', однако сперва:

  1. Запрос пройдет через middleware x-response-time выполнит все, что идет до строки yield next.
  2. Затем yield next остановит текущую функцию и передаст управление другому middleware logger, он также выполнит все до своего yield next.
  3. В логгере yield next передаст управление последнему middleware.
  4. Он в свою очередь сформирует ответ this.body = 'Hello World'.
  5. Больше middleware для передачи downstream у нас нет, так что начнется upstream процесс. Т.е. каждый middleware возобновляет свою работу после yield next, осуществляя таким образом механизм “вверх по течению”.

Круто в общем, что еще сказать. Подробнее про Koa можно посмотреть на официальном сайте и на гитхабе.

Заключение


Генераторы в ES6 представляют собой мощный механизм, который дает возможность писать более чистый и понятный асинхронный код. Вместо того, чтобы использовать ворох коллбеков по всему коду мы можем теперь писать асинхронный код, который с виду похож на синхронный, но на самом деле при помощи генераторов и ключевого слова yield “поджидает” завершения асинхронной операции.

Комментарии (6)