Run-to-completion

При виконанні асинхронного коду в JS, цикл подій слідує правилу run-to-completion (виконання-до-завершення). Це означає, що якщо якась функція почала виконуватись, жодна інша асинхронна функція не може перервати її виконання. Власне, саме ця особливість дозволяє “заблокувати” виконання однією “важкою” функцією. Також ця особливість спрощує розробку програм, адже позбавляє нас складнощів паралельного програмування, коли один потік може перервати інший в будь-якому місці.

Генератори

Генератори — це спеціальні функції, які порушують правило run-to-completion ****і можуть бути “призупинені” в своєму виконанні:

// Функції-генератори виглядають як звичайні, але з символом `*` біля назви
function *gen() {
	for(let i = 0; i < 10; i++) {
		// Для "призупину" виконання використовується ключове слово `yield`
		yield i;
	}
}

// Виклик функції-генератора не виконує її код одразу, 
//  на відміну від виклику звичайної функції.
//  Натомість, виклик генератора повертає обʼєкт-ітератор (див. [лекцію 3](<https://iliayatsenko.notion.site/3-696960801b9f4e5998353aa632c1dab3>))
var iter = gen();

// А вже ітератор контролює виконання тіла функції:
//  кожен виклик `next()` виконує код функції 
//  до наступної точки зупину (`yield`).
//  При цьому результат виразу, що знаходиться зправа від `yield`,
//  повертається в якості результату виклику `next()` 
//  (можна сказати "віддається генератором наззовні")
console.log(iter.next()); // {value: 0, done: false}
console.log(iter.next()); // {value: 1, done: false}
// ... і т.д.
console.log(iter.next()); // {value: 9, done: true}

// Виконання генератора завершено
console.log(iter.next()); // {value: undefined, done: true}

Ітерація по “згенерованим” значенням

Оскільки результат виклику функції-генератора це ітератор, по ньому можна пройти циклом for ... of ...:

function *gen() {
	for(let i = 0; i < 10; i++) {
		yield i;
	}
}

var iter = gen();
for (let item of iter) {
	console.log(item);
}

// 0 1 2 3 4 5 6 7 8 9

Назва “генератор” походить від здатності генерувати значення “на льоту”, тобто можна створювати ітератори, які не зберігають в памʼяті всі свої значення, на відміну від звичайних масивів, наприклад. Це буває корисно бо дозволяє зменшити використання памʼяті при ітерації великих колекцій даних, які ще не завантажені в оперативну памʼять. Але це не єдина і не основна область для використання генераторів.

yield - дорога в дві сторони

Результат виразу зправа від yield віддається з генератора “наззовні”, як результат виклику next(). А змінна зліва від yield (якщо така є) приймає “ззовні” те, що було передано в наступний виклик next(). Таким чином генератор може “обмінюватись” даними з викликаючим кодом: