Разница между генераторами ES6 и массивом функций
Читая блоги и статьи по javascript, я вижу большой интерес к генераторам ES6, но не понимаю, как они отличаются по сути от текущей последовательности, созданной с помощью массива функций. Например, нижеприведенная фабрика будет выполнять множество шагов функции и давать результат между шагами.
function fakeGen(funcList) {
var i = 0, context;
return function next() {
if (i<funcList.lenght) {
return {value: funcList[i++](context)}
} else return {done:true}
}
}
Какую выгоду я упускаю и как транспортеры реализуют магию в ES6?
2 ответа
@tophallen прав. Вы можете реализовать ту же функциональность полностью в ES3/ES5. Но не тот же синтаксис. Давайте рассмотрим пример, который, надеюсь, объяснит, почему синтаксис имеет значение.
Одним из основных применений генераторов ES6 являются асинхронные операции. Есть несколько раннеров, предназначенных для упаковки генераторов, которые производят последовательность Обещаний. Когда обернутый генератор дает обещание, эти бегуны ждут, пока это обещание не будет разрешено или отклонено, и затем возобновляют генератор, передавая результат назад или выбрасывая исключение в точке доходности, используя iterator.throw()
,
Некоторые бегуны, такие как tj / co, дополнительно позволяют выдавать массивы обещаний, возвращая массивы значений.
И вот пример. Эта функция выполняет два URL-запроса параллельно, затем анализирует их результаты как JSON, каким-то образом объединяет их, отправляет объединенные данные на другой URL-адрес и возвращает ответ (обещание):
var createSmth = co.wrap(function*(id) {
var results = yield [
request.get('http://some.url/' + id),
request.get('http://other.url/' + id)
];
var jsons = results.map(JSON.parse),
entity = { x: jsons[0].meta, y: jsons[1].data };
var answer = yield request.post('http://third.url/' + id, JSON.stringify(entity));
return { entity: entity, answer: JSON.parse(answer) };
});
createSmth('123').then(consumeResult).catch(handleError);
Обратите внимание, что этот код почти не содержит шаблонов. Большинство строк выполняют некоторое действие, которое существует в приведенном выше описании.
Также обратите внимание на отсутствие кода обработки ошибок. Все ошибки, как синхронные (например, ошибки синтаксического анализа JSON), так и асинхронные (например, неудачные запросы URL), обрабатываются автоматически и отклоняют полученное обещание.
Если вам нужно исправить некоторые ошибки (то есть не дать им отклонить полученное обещание) или сделать их более конкретными, то любой блок кода внутри генератора можно окружить try..catch
и ошибки синхронизации и асинхронности будут в конечном итоге catch
блок.
То же самое может быть определенно реализовано с использованием массива функций и некоторой вспомогательной библиотеки, такой как async:
var createSmth = function(id, cb) {
var entity;
async.series([
function(cb) {
async.parallel([
function(cb){ request.get('http://some.url/' + id, cb) },
function(cb){ request.get('http://other.url/' + id, cb) }
], cb);
},
function(results, cb) {
var jsons = results.map(JSON.parse);
entity = { x: jsons[0].meta, y: jsons[1].data };
request.post('http://third.url/' + id, JSON.stringify(entity), cb);
},
function(answer, cb) {
cb(null, { entity: entity, answer: JSON.parse(answer) });
}
], cb);
};
createSmth('123', function(err, answer) {
if (err)
return handleError(err);
consumeResult(answer);
});
Но это действительно ужасно. Лучше использовать обещания:
var createSmth = function(id) {
var entity;
return Promise.all([
request.get('http://some.url/' + id),
request.get('http://other.url/' + id)
])
.then(function(results) {
var jsons = results.map(JSON.parse);
entity = { x: jsons[0].meta, y: jsons[1].data };
return request.post('http://third.url/' + id, JSON.stringify(entity));
})
.then(function(answer) {
return { entity: entity, answer: JSON.parse(answer) };
});
};
createSmth('123').then(consumeResult).catch(handleError);
Короче, чище, но все же больше кода, чем в версии, которая использует генераторы. И еще какой-то шаблонный код. Обратите внимание на эти .then(function(...) {
линии и var entity
декларация: они не выполняют никаких значимых операций.
Меньшее количество шаблонов (= генераторов) делает ваш код более легким для понимания и изменения, и гораздо более увлекательным для написания. И это одна из самых важных характеристик любого кода. Вот почему многие люди, особенно те, кто привык к подобным понятиям на других языках, так в восторге от генераторов:)
Относительно вашего второго вопроса: транспортеры творит свою магию, используя затворы, switch
заявления и государственные объекты. Например, эта функция:
function* f() {
var a = yield 'x';
var b = yield 'y';
}
будет преобразован регенератором в этот (вывод Traceur выглядит очень похоже):
var f = regeneratorRuntime.mark(function f() {
var a, b;
return regeneratorRuntime.wrap(function f$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return "x";
case 2:
a = context$1$0.sent;
context$1$0.next = 5;
return "y";
case 5:
b = context$1$0.sent;
case 6:
case "end":
return context$1$0.stop();
}
}, f, this);
});
Как видите, здесь нет ничего волшебного, получившийся ES5 довольно тривиален. Настоящая магия заключается в коде, который генерирует полученный ES5, то есть в коде транспортеров, потому что они должны поддерживать все возможные крайние случаи. И желательно делать это таким образом, чтобы получился результативный выходной код.
UPD: вот интересная статья, которая датируется 2000 годом и описывает реализацию псевдо-сопрограмм на простом языке C:) Техника, которую Regenerator и другие транспортеры ES6 > ES5 используют для захвата состояния генератора, очень похожа.
Генератор - это, по сути, функция перечислителя, она позволяет изменять контекст, с которым вы работаете, во время его вызова, на самом деле между ним и вашим массивом функций нет большой разницы, однако преимущество, которое вы получаете, заключается в том, что он не должны быть функциями внутри оцениваемых функций, что упрощает замыкания. Возьмите следующий пример:
function* myGenerator() {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
}
это очень простой пример, но вместо того, чтобы создавать контекст, который вам нужно предоставить кому-то для перечисления результатов, он предоставляется вам, и вы уверены, что done
свойство будет ложным, пока не будет завершено. Эта функция выглядит намного чище, чем приведенный вами пример. Вероятно, самое большое преимущество состоит в том, что оптимизация вокруг этого может происходить под капотом, так что объем памяти объекта оптимизируется.
Приятно отметить, что вы действительно очищаете код при перечислении нескольких коллекций объектов, например:
function* myGenerator() {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
for (var i = 0; i < arr2.length; i++) {
yield arr2[i];
}
yield* myGenerator2();
}
выполнение этого с помощью только что вложенных вложенных функций может привести к тому же результату, но удобство сопровождения и читаемость кода несколько страдают.
Что касается транспортеров, из потока CS:
Там нет конфликта. Coffeescript будет просто генерировать любой javascript, необходимый для компиляции любого синтаксиса, старого или нового.
В прошлом coffeescript не использовал функцию javascript, пока все браузеры не поддерживают ее. Это, вероятно, относится и к генераторам. До тех пор вы должны будете использовать обратные пометки.
Мое общее понимание большинства транспортеров заключается в том, что они должны быть осторожны при реализации функциональности, которая не будет проходить назад и будет вообще совместима, и, как таковые, обычно опаздывают в игру.
Как вы сказали, генератор не делает ничего сверхспособного, это всего лишь синтаксический сахар, который облегчает чтение, обслуживание, использование или выполнение кода.