Как стать автором
Обновить

Комментарии 61

Мне кажется, статья про генераторы не может быть актуальной, если в ней нет упоминания regenerator от Facebook.
Мне кажется или в последнем примере значения res1 и res2 могут произвольно меняться и непонятно, в какой из них прилетит результат скачки «test1.txt»?
ага, мне тоже кажется что будет чехарда.
_get("test1.txt", resume());
_get("test2.txt", resume());

Судя по всему коллбэки (которые являются результатом исполнения resume()) строятся в стэк, результаты из которого и передаются по очереди в генератор. Это позволяет знать порядок, в котором необходимо отдавать значения, соответственно нужные значения в таком случае поставятся вместо нужных yield.
Я могу ошибаться, более точно изучить поведение genny можно тут: https://github.com/spion/genny/blob/master/index.js
Вангую, что с широким внедрением harmony в браузеры, многие фронтендеры начнут вместо чего-то такого:

parallel({
    a: function (callback) {
        asyncFunction1(args, callback);
    },
    b: function (callback) {
        asyncFunction2(args, callback);
    }
}, function (results) {
    var smthA = results.a;
    var smthB = results.b;

    // ...
});

Будут писать что-то такое:

var smthA = yield asyncFunction1(args);
var smthB = yield asyncFunction2(args);

// ...

А потом удивляться, что такой красивый код работает в 2 раза дольше того старого с лапшой из колбэков.
Почему он должен работать дольше? Тем более в два раза?
Потому что пока не выполнится asyncFunction1, не будет выполняться asyncFunction2. А поскольку они обе асинхронные, то их параллельное (псевдопараллельное в рамках одного event loop) выполнение сработало бы гораздо быстрее.
А, ок, думал очередный «выплеск любви» к коллбэкам :-)
Пример с genny делает ровно то же, что и ваш первый пример: так же выполняет две асинхронные операции перед тем, как продолжить выполнение генератора.
Да, но от реализации такого кейса в genny сводит живот, а от parallel нет. Дело вкуса как обычно.
Берем spawn например отсюда, оборачиваем асинхронные функции в промисы, вспоминаем (не знаю зачем) про деструктивное присваивание в ECMAScript 6 и получаем что-то в стиле
spawn(function*(){
  let [smthA, smthB] = yield Promise.all([
    asyncFunction1(args),
    asyncFunction2(args)
  ]);
  // ...
});
Можно и так, но я бы не стал такое использовать на фронтенде по понятным причинам.
Это по каким же? :) Ссылку на перевод данной статья я привел выше, причин не использовать подобное, как вы написали, «с широким внедрением harmony в браузеры» я там не вижу.
Хотя бы потому что Promise.all() возвращает promise, а не функцию-генератор в текущем API. То есть в тех самых крутых браузерах этот код будет работать только если переопределить Promise
Забавно. Зачем Promise.all() возвращать генератор? :) Внимательно посмотрите на функцию spawn из этой статьи. Если на пальцах — она ориентирована на передачу в yield промисов и возвращает в генератор результат их разрешения.
Подозревал, но очень надеялся Джейк не стал постить такой ад на html5rocks. Ошибался :)
Аргументируйте. Или опять будет что-то про, якобы, возврат генератора из Promise.all()? :)
А что комментировать-то? Я терпеть не могу промисы и уже пообсуждал с Джейком это в твиттере — он понимает, что в ноде они никогда не появятся. Генераторы мешать с промисами никогда не буду по доброй воле. И да, почти забыл — :)
Promises — это спецификация js, а не часть движка. Да, в ноде эти промисы никогда не появятся (ура!)
Одно другого не отменяет. Ладно. Предлагаю спор на ящик хорошего пива (его не пью, но ради интереса), что в течении месяца, максимум двух, промисы будут доступны в нестабильной ветке ноды с флажком --harmony.
Слив засчитан.
my pleasure :)
Выдав свою нездоровую фантазию за действительность, главное вовремя слиться. тык

А вот и появились ;)
Будет занятно, если Федя-таки протолкнет промисы в ядро. Посмотрим.
Конечно интересная штука, но таскать везде костыльный resume вообще не хорошо.
Чем не костыль делать обертку для каждой асинхронной функции в генераторе?
Из двух зол как говорится.
Я при чём тут генератор? Я же говорю, что подход работы с ajax без callback'ов интересен, но появление костыля — это не хорошо.
например в co.js колбэки не нужны, но нужно врапить функции
co(function *(){
  var a = yield get('http://google.com');
  var b = yield get('http://yahoo.com');
  var c = yield get('http://cloudup.com');
  console.log(a.status);
  console.log(b.status);
  console.log(c.status);
})()
В примере из статьи тоже нужно оборачивать.
А вот данный пример мне больше нравится, только нужно посмотреть что там будет с трейсом, не сломается ли он от этого.
Не могли бы вы поподробнее пояснить принцип работы последнего куска кода с одновременным выполнением?
_get(«test1.txt», resume());
_get(«test2.txt», resume());

Посылаем два ajax запроса

var res1 = yield resume,
res2 = yield resume; // step 1

Ждем ajax ответа и пихаем в res1, ждем следующий ответ и пихаем в res2. Если первым ответ придет от test2.txt, значит он и будет в res1.

var res3 = yield _get(«test3.txt», resume()); // step 2

Сам себя вызвал, сам результат вернул.

console.log(res1 + res2);

Вывод.
НЛО прилетело и опубликовало эту надпись здесь
var res1 = yield _get(«test1.txt», resume());
var res2 = yield _get(«test2.txt», resume());
НЛО прилетело и опубликовало эту надпись здесь
Что async?
Реализация генераторов, предлагаемая в JavaScript 1.7+, не придерживается черновика ECMAScript 6 — так что вам придется внести некоторые изменения, чтобы заставить примеры работать в Firefox. Если вы хотите запустить эти примеры в Canary, вы можете запускать их в таком же виде, как здесь.


ну вот так всегда… опять каждый тянет воз в свою сторону…
Да все нормально. Просто в Mozilla эта фича была уже довольно давно, еще до попытки вписать её в стандарт. К выходу ES6 у всех все будет правильно.
Стоит упомянуть о koajs.com/ (Express на генераторах)
Очень изящно. Но как быть, если у коллбеков разные сигнатуры?
Так resume по сути один на всё(с одной сигнатурой), она иcпользуется вместо «return value», главное её таскать за собой по всему коду.
Собственно, да.
А если есть необходимость сначала использовать require('fs').stat(), где коллбек вызывается с (int err, object result), а затем какой-нибудь megaLib.doJob(), вызывающий коллбек с (object result)? Насколько я понял, итератор поперхнётся, приведя result к boolean и получив true.
После Tornado с корутинами пробовал Node.JS и именно из-за отсутствия yield не осилил (не понравилось).
Здесь наверно стоит упомянуть, что coroutine — это реализация аналогичной концепции на Python, которая появилась достаточно давно. Правда вся их мощь откроется только с релизом Python 3.4, где можно будет писать асинхронный код без кастомной реализации event loop вроде Tornado — модуль asyncio.
Все это прикольно конечно, но в начальных версиях тот же co вообще херил стек трейсы при выкидывании исключений. Как то все забивают на это, и это ужасно.
Ну и зачем отдельный синтаксис для приостанавливаемых функций и отдельный же синтаксис для ожидания результата? К чему это выпячивание внутренней реализации? те же node-fibers позволяют элегантно инкапсулировать асинхронность не заставляя каждую промежуточную функцию превращать в генератор и распихивать yield-ы. К тому же, благодаря Proxy есть возможность приостанавливать не сразу, а лишь когда в этом появится надобность (https://github.com/nin-jin/node-jin#sod).
Возможно я ошибаюсь, но вы говорите об использовании волокон, в то время как статья про генераторы. Насколько я понимаю fiber — это всё таки отдельная нить, и быстродействие такой системы сильно падает. Хотя, конечно, код менее извёрнутый.
Про производительность неплохо бы привести какие-нибудь ссылки… как говорится proof or GFTO.
Вместо пруфа я написал «насколько я понимаю» :) Потому что в этих делах не силён. Насколько я понимаю создание отдельной нити — занятие не дешёвое. В то время как yields скорее всего работают по принципу стека.

Но хочу отметить, что волокна в JS, как минимум, на хабре обсуждались уже не единожды, и чаще всего именно критически. Мой же опыт работы с node-fibers… В общем я был сильно недоволен стабильностью работы и отладки.
Fiber — это легковесная нить с кооперативной многозадачностью, которая не требует переключения контекста на уровне процессора. Она более низкоуровневая, чем yield, и, как правило, быстрее.
Пытаясь применить информацию из этой статьи на практике наткнулся на то, что в метод raise отсутствует в nodejs, в то время как вместо него присутствует throw. Но оный не прерывает выполнение «функции». Моя промежуточная тестовая sync-функция стала выглядеть вот так:
global.sync = function sync( gen )
{
	var iterable, resume;

	resume = function( err, ret_val )
	{
		if( err )
		{
			iterable.throw( err );
			return;
		}

		iterable.next( ret_val );
	};

	iterable = gen( resume );
	iterable.next();
}


Сомневаюсь что в Canary используется другой движок. Ну или за время написания этой статьи уже что-то успели поменять.
Проверил. В Canary 34.0.1808.0 метод так же называется throw. Тем не менее, вот пример с корректной обработкой исключений, работающий в Canary: jsfiddle.net/UERy3/
Уже давно использую свою библиотеку с использованием генераторов на стороне nodejs.
Сейчас уже не представляю как можно писать читабельный бекэнд без генераторов, либо уж ИМХО java php или ruby

Если кому интересно могу выложить на гитхаб. Никаких проблем пока не возникает, исключения работают на ура

Пример модуля рассылки нотификаций вконтакте
var rest = require("../rest");
module.exports = function (app) {

    function *vkPush(vkusers, message) {
        if (!(vkusers instanceof Array))
            throw new Error("vkusers is not array");

        var APP_ID = 11111;
        var APP_SECRET = "xxxxx";
        var uids = [];
        try {
            var url = 'https://oauth.vk.com/access_token?client_id=' + APP_ID + '&client_secret=' + APP_SECRET + '&grant_type=client_credentials';
            var data = yield rest.get(url, vkPush.next); //vkPush.next можно записать как arguments.callee.next
            var token = data.access_token;
            var url = 'https://api.vk.com/method/secure.sendNotification?access_token=' + token + '&v=5.7&user_ids=' + vkusers.join(',') + '&message=' + encodeURI(message) + '&client_secret=' + APP_SECRET;
            var data = yield rest.get(url, vkPush.next);
            if (data.response) {
                uids = uids.concat(data.response.split(","));
                console.log("Received", uids);
            }
        }
        catch (e) {
            console.error(e.stack || e);
        }

        return uids;
    }

    return function *fn(req, res) {
        var table = "notifications";
        var user_id = req.user.id;
        var client_id = req.user.id;
        var id = +req.body.id || +req.query.id;


        // Для асинхронных операций
        var data = {};
        app.db.brodie("Query1", function (err, data) {
            data.query1 = data;
        }.async());

        app.db.brodie("Query2", function (err, data) {
            data.query2 = data;
        }.async());

        app.db.brodie("Query3", function (err, data) {
            data.query3 = data;
        }.async());

        // здесь будет остановка до тех пор пока все запросы не отработают
        yield true;

        try {
            switch (req.method) {
                case "GET":
                    var rows = yield app.db.brodie.query("SELECT * FROM ?? WHERE client_id=? ORDER BY date DESC", [table, client_id], fn.next);
                    res.send(rows);
                    break;

                case "PUT":
                    var message = req.body.text;
                    var data = {
                        client_id: client_id,
                        user_id: user_id,
                        text: message,
                        received: 0,
                        date: new Date()
                    }
                    var vkusers = yield app.db.brodie.query("SELECT * FROM notification_users WHERE client_id=? AND type='vk'", [client_id], fn.next);
                    var receiveUids = yield vkPush.run(vkusers, message);
                    if (receiveUids.length)
                        yield app.db.brodie.query('UPDATE notification_users SET last_send=NOW() WHERE uid IN (?) AND type="vk" AND client_id=?', [receiveUids, client_id], fn.next);

                    data.received = receiveUids.length;
                    var result = yield app.db.brodie.query("INSERT INTO ?? SET ?", [table, data], fn.next);
                    var rows = yield app.db.brodie.query("SELECT * FROM ?? WHERE id=?", [table, result.insertId], fn.next);
                    res.send(rows[0]);
                    break;


                default:
                    res.send(500);
                    break;
            }
        }
        catch (e) {
            console.error(e.stack || e);
        }

    }.fn();
}

Вы простите, но данный пример это не читабельный код.
Прерывать выполнение одной здоровой функции, а потом ещё switch со здоровыми телами.
Затем взявшийся из внешней области видимости res.

Вас, кстати не смущает, что вы несколько раз в одном блоке объявляете одну и те же переменные?

var url =…
var data =…

var url =…
var data =…
Простите, но в чем преимущество такого подхода написания кода, перед простыми callbacks?
Или Вы ради галочки, то, что используете generators?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории