Генератори та yield
Генератор (function*) це функція, яка може призупиняти своє виконання, віддавати проміжні значення і потім продовжувати роботу з того самого місця. Формально function* повертає спеціальний об’єкт, ітератор, який керує виконанням тіла функції.
Теорія
TL;DR
function*оголошує генератор; його виклик не виконує код, а повертає об’єкт-ітератор.yieldпризупиняє виконання, віддає значення назовні у{ value, done }і чекає на наступний.next().gen.next(value)повертає значення всередину: воно стає результатом тогоyield, на якому генератор зупинився..return(value)завершує генератор достроково,.throw(error)кидає в нього виняток, який можна впіймати черезtry...catch.- Генератор автоматично реалізує і протокол ітератора (
next()), і протокол ітерованого об’єкта ([Symbol.iterator]), тому працює уfor...of. - Генератори ідеальні для лінивих обчислень і нескінченних послідовностей: дані видаються порціями, без створення величезних масивів.
Швидкий приклад
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }Як оголошується генератор і що повертає його виклик
Генератор оголошується зірочкою після ключового слова function:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}Найважливіший момент: під час виклику myGenerator() сам код функції не виконується. Виклик повертає ітератор, який треба «прокручувати» вручну, викликаючи .next(). Кожен такий виклик доводить функцію до наступного yield і повертає об’єкт { value, done }, де value це віддане значення, а done показує, чи функція вже дійшла до кінця.
Генератором може бути і метод об’єкта чи класу, для цього зірочку ставлять перед іменем: *items() { ... }. А от стрілкова функція генератором бути не може.
Що робить yield
Оператор yield робить три речі:
- Призупиняє виконання функції рівно на цьому місці.
- Віддає значення назовні, у результат
.next(). - Дозволяє передати значення назад усередину під час наступного виклику
.next().
Приклад 1, проста послідовність:
function* counter() {
yield 1;
yield 2;
yield 3;
}
const gen = counter();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }Приклад 2, паузи між кроками:
function* steps() {
console.log("Крок 1");
yield;
console.log("Крок 2");
yield;
console.log("Крок 3");
}
const gen = steps();
gen.next(); // "Крок 1"
gen.next(); // "Крок 2"
gen.next(); // "Крок 3"Тут yield використовується без значення, просто як точка зупинки.
Як передати значення назад у генератор
Коли ви викликаєте gen.next(value), цей value потрапляє всередину генератора саме в те місце, де виконання було призупинене оператором yield. Тобто вираз yield "..." обчислюється в те, що ви передали наступним .next().
Приклад 3, передача значення назад:
function* dialog() {
const name = yield "Як тебе звати?";
const age = yield `Привіт, ${name}! Скільки тобі років?`;
yield `Тобі ${age} років.`;
}
const gen = dialog();
console.log(gen.next()); // { value: "Як тебе звати?", done: false }
console.log(gen.next("Maria")); // { value: "Привіт, Maria! Скільки тобі років?", done: false }
console.log(gen.next(25)); // { value: "Тобі 25 років.", done: false }
console.log(gen.next()); // { value: undefined, done: true }Пояснення по кроках:
- Перший
next()запускає генератор і повертає значення першогоyield. - Другий
next("Maria")вставляє рядок"Maria"туди, де стояв першийyield, томуnameотримує це значення. - Третій
next(25)вставляє25на місце другогоyield, іageдорівнює25.
Керування генератором: next, return і throw
У ітератора генератора є три методи керування:
| Метод | Що робить |
|---|---|
.next(value) | Продовжує виконання і повертає наступний { value, done } |
.return(value) | Примусово завершує генератор, повертає { value, done: true } |
.throw(error) | Кидає виняток усередину генератора, його можна впіймати через try...catch |
Приклад 5, return() і throw():
function* process() {
try {
yield "Крок 1";
yield "Крок 2";
} catch (e) {
console.log("Помилка:", e.message);
} finally {
console.log("Генератор завершено");
}
}
const gen = process();
console.log(gen.next()); // { value: "Крок 1", done: false }
console.log(gen.throw(new Error("Щось пішло не так")));
// Помилка: Щось пішло не так
// Генератор завершеноБлок finally спрацьовує і тоді, коли генератор завершують через .return(), тому саме туди зручно класти прибирання ресурсів.
Генератори як ітератори, ліниві та асинхронні послідовності
Генератор автоматично реалізує інтерфейс ітератора (next()) і інтерфейс ітерованого об’єкта ([Symbol.iterator]), тому його можна напряму передати у for...of, у spread або в деструктуризацію:
function* numbers() {
yield 1;
yield 2;
yield 3;
}
for (const n of numbers()) {
console.log(n); // 1, 2, 3
}Приклад 4, нескінченна послідовність:
function* infinite() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = infinite();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2Такий код не зависає, бо значення обчислюються ліниво: наступне число з’являється лише тоді, коли його попросили через .next().
Бонус, генератори і асинхронність. До появи async/await генератори використовували саме для асинхронного коду (через бібліотеки на кшталт co):
function* getData() {
const user = yield fetch('/user');
const posts = yield fetch(`/posts?user=${user.id}`);
return posts;
}Пізніше на цій самій ідеї і побудували async/await, по суті це «автоматизовані генератори», де рушій сам викликає .next(), коли проміс виконався.
Підсумкова таблиця:
| Поняття | Опис |
|---|---|
function* | Оголошення генератора |
yield | Призупиняє виконання і віддає значення назовні |
.next(value) | Відновлює виконання, передаючи value всередину |
.return(value) | Завершує генератор |
.throw(error) | Кидає виняток у генератор |
| Генератор | Об’єкт-ітератор, виконання якого можна призупиняти і продовжувати |
Типові помилки
- Думати, що виклик
myGenerator()виконує тіло функції. Він лише створює ітератор; перший рядок коду виконається тільки на першому.next(). - Очікувати, що аргумент першого
.next(value)кудись потрапить. Перший виклик лише запускає генератор, і передане значення ігнорується, бо ще немаєyield, який міг би його прийняти. - Плутати напрямки. Назовні летить значення праворуч від
yield, а всередину повертається аргумент наступного.next(). - Прогонити нескінченний генератор через
for...ofбезbreak. Цикл ніколи не завершиться, боdoneне станеtrue. - Забувати, що генератор одноразовий. Після
done: trueабо після.return()усі наступні.next()дають{ value: undefined, done: true }; щоб пройти послідовність ще раз, треба створити новий генератор. - Намагатися оголосити генератор стрілковою функцією.
function*працює лише зі звичайними функціями та методами (*method() {}). - Вважати генератор асинхронним сам по собі. Звичайний генератор синхронний; для асинхронних послідовностей потрібні
async function*іfor await...of.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.