Знайомство з асинхронними ітераторами та генераторами

3 хв. читання

TLDR

Б'юся об заклад, ви й не знали, що наступний блок коду — цінна річ:

for await (const info of getApi(apis)) {
 console.log(info);
}

Це насправді так, і тепер він має хорошу браузерну підтримку. Зразок коду виконує ітерацію по асинхронній операції getApi, без необхідності чекати операцію дозволу.

Асинхронна поведінка

Перехід до ітераторів без попереднього згадування того як в JS працює асинхронність, може трохи вводити в оману. В принципі, коли ви виконуєте операцію асинхронно, у вас є місце для переходу до іншої задачі, перш ніж вона завершить виконання, і це саме те, що з себе представляють асинхронні функції. Нижче наведена функція setTimeout — найбільш базова форма асинхронності JavaScript, що коли-небудь була написана.

  // Сказати "I have Mac and Cheese"
  console.log("I have Mac and Cheese");
  // Сказати "Sure thank you" через три секунди. 
  setTimeout(function() {
    console.log("Sure thank you");
  }, 3000); 
  // Сказати "Want Some?"
  console.log("Want some?");

  //console logs:
  > I have Mac and Cheese
  > Want some?
  > Sure thank you

Синхронна ітерація

Ітерація — це, в основному, спосіб проходження даних. Вона працює за допомогою ряду концепцій, до яких належать:

Iterables: структури даних, які можуть бути проітеровані. Вони показують, що їх можна проітерувати за допомогою використання протоколу Symbol.iterator. Цей протокол визначає, що об'єкт має демонструвати ітераційну поведінку.

Іterator: об'єкт, що повертається за допомогою виклику [Symbol.iterator]() на ітерабельному елементі. Використовуючи свій метод next(), він огортає кожний ітерований елемент в структурі даних й повертає їх один за одним.

IteratorResult: нова структура даних, що повертається за допомогою next().

Приклади структур даних, що можуть бути ітерабельними, включають масиви, рядки та мапи. Код нижче демонструє, як працює синхронне ітерування:

  const iterable = ['x', 'y', 'z'];
  const iterator = iterable[Symbol.iterator]();
  iterator.next() {
    value: 'x',
    done: false
  }
  iterator.next() {
    value: 'y',
    done: false
  }
  iterator.next() {
    value: 'z',
    done: false
  }
  iterator.next() {
    value: undefined, 
    done: true
  }

У наведеному вище прикладі, властивість value має ітерований елемент, next() продовжує повертати кожен елемент у структурі даних доти, доки вона не буде закінчена, тому значення є undefined. Для кожного дійсного значення, що повертається, властивість done є false, після останнього елемента вона повертається у true.

Розгляньмо більш детальний приклад. Тут Symbol.Iterator використовується ще раз для ітерації через масив letter в ітерабельному codebeast:

  const codebeast = {
    [Symbol.iterator]: () => {
      const letter = [`c`, `o`, `d`, `e`, `b`, `e`, `a`, `s`, `t`]; 
      return {
        next: () => ({
          done: items.length === 0,
          value: items.shift()
        })
      }
    }
  }

Ми можемо ітерувати через об'єкт codebeast, використовуючи цикл for... of:

  for (const letter of codebeast) {
    console.log(letter)
    // <- `c`
    // <- `o` 
    // <- `d` 
    // <- `e`
    // <- `b`
    // <- `e`
    // <- `a`
    // <- `s`
    // <- `t` 
  }

Асинхронна ітерація

Асинхронна ітерація працює майже так само, як і синхронна, окрім того, що вона передбачає проміси (promises). Потреба в ітеруванні через асинхронні джерела даних призвела до виникнення асинхронної ітерації. У наведеному нижче коді readMemo() не може повернути свої дані асинхронно:

  for (const word of ourMemo(memoName)) {
    console.log(word);
  }

З асинхронною ітерацією концепти Iterable, Iterator та IteratorResult працюють трохи інакше. В асинхронній ітерації ітерабельні елементи використовують методи Symbol.asyncIterator. Замість того, щоб безпосередньо повертати результати Iterator, метод next() асинхронного ітератора повертає проміс. Спробуємо в нижче наведеному коді зробити codebeast асинхронно ітерабельним:

  const codebeast = {
    [Symbol.asyncIterator]: () => {
      const letter = [`c`, `o`, `d`, `e`, `b`, `e`, `a`, `s`, `t`];
      return {
        next: () => Promise.resolve({
          done: letter.length === 0,
          value: letter.shift()
        })
      }
    }
  }

Ми можемо асинхронно виконувати нашу ітерацію, використовуючи цикл for... await... of:

  for (const letter of codebeast) {
    console.log(letter)
    // <- `c` 
    // <- `o`
    // <- `d`
    // <- `e`
    // <- `b`
    // <- `e`
    // <- `a`
    // <- `s`
    // <- `t`
  }

Ще один спосіб показати асинхронну ітерацію — створити функцію, яка може послідовно викликати декілька веб-інтерфейсів:

  const displayApi = server => ({
    [Symbol.asyncIterator]: () => ({
      x: 0,
      next() {
        if (server.length <= this.x) {
          return Promise.resolve({
            done: true
          })
        }
        return fetch(server[this.x++])
          .then(response => response.json())
          .then(value => ({
            value,
            done: false
          }))
      }
    })
  })

  const apis = [
    `/api/random-names`,
    `/api/random-beer`,
    `/api/random-flag`,
    `/api/random-book`
  ];

  for await (const info of getApi(apis)) {
    console.log(info);
  }

Зверніть увагу, як код, що виглядає синхронним, буде працювати асинхронно? Використання циклу for...await...of в асинхронній ітерації — це чудово, оскільки він передає кожне значення тільки після того як asyncIterator.next() буде вирішений. Він також гарантує, що asyncIterator.next() не викличеться для наступного елементу масиву, доки поточна ітерація не завершиться. Це дозволяє не перекриватися вашим відповідям, тому вони будуть повернені правильним чином.

Генератори

Ще одна цікава особливість ES6. Генератори головним чином використовуються для представлення послідовностей, які можуть бути нескінченними. Генератори також можуть бути процесами, які можна відновити й призупинити. Синтаксис генераторів наступний:

  function* genFunc() {
    console.log('One');
    yield;
    console.log('Two');
  }

Де function* — нове ключове слово, яке використовується для функцій генератора, yield — оператор, який генератор може використовувати для призупинення, а також для приймання вводу та відправлення виводу. Досить розмов, приступімо до роботи з генераторами. Код нижче використовує генератор для повернення додатних цілих чисел:

  function* countUp() { 
    for (var i = 0; true; i++) { 
      yield i
    }
  }
  for (var i of countUp()) {
    console.log(i)
  }

У наведеному вище прикладі функція countUp обчислюється «ліниво», вона призупиняється на кожному yield й чекає запиту іншого значення. Це означає, що цикл for... of буде продовжувати виконуватися, оскільки наш список цілих чисел нескінченний. Це дає багато можливостей, що можуть нам допомогти реалізувати асинхронні операції, такі як цикли й умовні вирази в наших функціях. В цілому, генератори не мають способу представлення результатів асинхронних операцій. Для цього вони повинні працювати з промісами. Говорячи про проміси, подивімося як ми можемо виконати ітерацію через генераторну функцію, використовуючи метод .next:

  function* race() {
    var lap1 = yield 20; 
    assert(lap1 === 35); 
    return 55;
  }

  var r = race();
  var lap2 = r.next();
  // => {value: 20, done: false}
  var lap3 = r.next(35); 
  // => {value: 55, done: true} 
  //if we call r.next() again it will throw an error

У вище наведеному прикладі r.next() викликалась перший раз, щоб перейти до yield, а потім викликалась другий раз й передала значення, яке є результатом виразу yield. Таким чином, race() потім може перейти до return, щоб повернути кінцевий результат. Це може бути реалізовано за допомогою виклику .next(result), щоб показати, що проміс виконано разом з результатом.

Але що, якщо наш отриманий проміс відкидається? Ми можемо це показати, використовуючи метод .throw(error):

  var shortcut = new Error('too fast'); 
  function* race() {
    try { 
      yield 100;
    } catch (h) {
      assert(h === shortcut);
    }
  }
  var r = race();
  r.next();
  // => {value: 100, done: false}
  d.throw(shortcut);

Як і в минулому прикладі, r.next() викликається, щоб отримати перше ключове слово yield. Ми використовуємо r.throw(error), який послужить сигналом про відмову, оскільки це спричинило те, що наш генератор поводився так, наче помилка була викликана yield. Це автоматично запускає блок catch.

Візьмемо ще один приклад, в якому ми спробуємо зробити двосторонній зв'язок з генераторами. Тут next() може фактично призначити значення, отримані від генератора:

  function* techInterview() { 
    const answer = yield 'Who is the CEO of Tesla?';
    console.log(answer);
    if (answer !== 'Elon Musk') 
    return 'No Way!'
    return 'Okay, on to the next question';
  }

  { 
    const Iterator = techInterview();
    const q = Iterator.next() .value; // Ітератор ставить питання
    console.log(q); 
    const a = Iterator.next('Scott Hanselmann') .value;
    // Передає неправильну відповідь назад у генератор
    console.log(a);
  } 
    // Who is the CEO of Tesla? 
    // Scott Hanselmann
    // No Way! 

  {
    const Iterator = techInterview();
    const q = Iterator.next() .value; // Ітератор ставить інше питання
    console.log(q);
    const a = Iterator.next('Jimmy Kimmel') .value; 
    // Передає неправильну відповідь назад у генератор
    console.log(a);
  } 
    // Who is the CEO of Tesla?
    // Jimmy Kimmel
    // No Way!

  { 
    const Iterator = techInterview();
    const q = Iterator.next() .value;  // Ітератор ставить інше питання
    console.log(q); 
    const a = Iterator.next('Elon Musk') .value;
    // Передає правильну відповідь назад у генератор
    console.log(a);
  }
    // Who is the CEO of Tesla? 
    // Elon Musk
    // Okay on to the next question

Кілька ресурсів для подальшого читання (англ.):

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація