Pull to refresh

Comments 22

Вы не могли бы ещё раз рассказать, почему альтернативный подход с IteratorResult не годится? Немного непонятно получилось в статье.

Ну вот разгружаете вы вагоны с дерьмомзерном. Подставляете спину, туда прилетает мешок, вы его быстренько-быстренько в закрома отсыпаете там, конечно, и за следующим, пока мешки летают. Но вот рабочий день закончился, вагон опустел, но хотя последний рабочий, уходя, похлопал вас по спине, аки ломовую лошадь, вы разогнуться уже не можете, так и стоите в интересной позе тылом к рельсам. А ведь могли бы домой пойти, если бы на последнем мешке была этикетка с надписью "все, баста!"...

Всё равно не понял)) Если без метафор, то в чём загвоздка?


Это сделано для тех случаев, когда мы изнутри синхронного генератора не можем определить есть ли следующее значение или нет. Например нужно сходить в некую удалённую очередь по сети и забрать следующее значение, если очередь опустела, то выйти из цикла.

Судя по коду, мы внутри генератора делаем await (дальнейший код исполнится когда "фоновый" Promise заресолвится, если я правильно понимаю), и потом проверяем на переменную на null как условие, что данных больше нет. Так может это условие и использовать как знак того, что "баста"?

А снаружи-то как вовремя узнать что больше элементов не будет?


Чему будет равно свойство done итератора во время выполнения оператора await в генераторе?

А снаружи-то как вовремя узнать что больше элементов не будет?

Снаружи "вовремя" будет тогда, когда зарезолвится промис, т.к. пока это не произойдёт ваш цикл не сможет пойти дальше. А когда промис резолвится, то он возвращает состояние итератора, где указано завершился итератор или нет.


Чему будет равно свойство done итератора во время выполнения оператора await в генераторе?

Оно будет равно false. Станет равным true лишь при выходе из функции-генератора явно (по return) или неявно (кончилось тело функции).

Вот смотрите, проверили мы done. Оно равно false. Мы получили очередной промиз и начали его ждать.


А следующего элемента-то и нет! Как теперь закончить ожидание?


PS блин, да вы ниже сами все расписали с кодом! Зачем тут чушь пишите?

Когда вызывается return или мы выходим из генератора, то всё равно возвращается Promise, содержащий IteratorResult. Вот пример с кодом:


async function* values () {
  yield 1
  yield 2
}

function handle () {
  const iter = values()
  console.log(iter.next()) // Promise { value: 1, done: false }
  console.log(iter.next()) // Promise { value: 2, done: false }
  // следующего элемента нет
  console.log(iter.next()) // Promise { value: undefined, done: true }
}
handle()

Т.е. последний промис разрешается сразу и возвращает done = true. Вы можете запустить этот код и проверить самостоятельно.


Зачем тут чушь пишите?

В каком месте я написал чушь?

Это вы сейчас написали как Promise<IteratorResult> работает.


А я отвечал на вот этот вопрос:


Вы не могли бы ещё раз рассказать, почему альтернативный подход с IteratorResult не годится? Немного непонятно получилось в статье.

Альтернативный — это тот, при котором метод next() возвращает IteratorResult<Promise>.

Загвоздки нет, просто код будет выглядеть менее уродски. Вот аналог с обычным итератором:
for(const taskPromise of queue) {
  const task = await taskPromise;
  task ? resolve(task) : break;
}

Просто итератор будет бесконечный и проверку на выход прийдется делать руками каждый раз. В случае же асинхронного итератора у вас проверка будет спрятана в сам итератор и он не будет бесконечным.

Давайте избавимся от for-await-of и посмотрим как это можно обработать вручную. Допустим, у нас есть удалённая очередь queue с несколькими элементами.
Пример для IteratorResult<Promise>:


const queue = ... // queue это итератор
while (true) {
  const { value, done } = queue.next()
  // done всегда будет false, т.к. из генератора синхронно(!) мы не можем узнать закончилась ли очередь
  const result = await value // здесь мы дожидаемся разрешения value
  if (result === null) { break } // вот здесь нужно проверить, что нам вернулось пустое значение. Но дело в том, что null может быть вполне валидным значением, а не индикатором пустой очереди
  // обрабатываем  result; следующая итерация
}

Как видите, этот подход имеет недостаток — у нас нет четкого понимания, что очередь пуста. Мы не можем трактовать null как конец, если только не приняли некое соглашение, что null — это всегда конец очереди.


В случае с Promise<IteratorResult> всё несколько иначе:


const queue = ...
while (true) {
  const { value, done } = await queue.next() // здесь у нас есть Promise, который возвращает текущее состояние итератора
  if (done) { break } // и есть четкое понимание когда стоит прекратить цикл
  // обрабатываем value; следующая итерация
}

Т.е. при подходе Promise<IteratorResult> у нас есть возможность без всяких соглашений четко дать понять, что очередь пуста, можно выходить из цикла. queue, например, может при каждом вызове next() помимо получения элемента спрашивать у очереди сколько элементов осталось и при значении 0 вернуть done = true, чтобы прервать цикл и не создать последующих запросов.

Спасибо, почти понятно. А если сильно извратиться — можно ли чисто теоретически снаружи повлиять на наш генератор, основываясь на информации, которую вернёт Promise? Например, опять же, передавать помимо значения некий флаг конца в поле объекта.


Хотя понимаю, что это сильный костыль.

А если сильно извратиться — можно ли чисто теоретически снаружи повлиять на наш генератор

Да, вы можете в next() передать какое-нибудь значение:


function* generator () {
  let value = yield 'Hi!'
  console.log('Hello %s!', value)
}

const iterator = generator()
console.log(iterator.next().value) // выведет "Hi!"
iterator.next('World') // выведет "Hello World!"

Таким образом можно передать генератору что угодно.

А можно ли будет применять функции map reduce filter и пр. над асинхронными итераторами? Получилась бы хорошая замена observable.

Нет. Но наверняка там появятся асинхронные версии этих функций.

Сейчас и для обычных итераторов их нельзя применить. Для этого сначала нужно преобразовать в массив (с помощью Array.from или spread оператора [ ...iterable ]), а потом над ним уже совершать операции.
В качестве эксперимента я пишу библиотеку, которая добавляет эти методы прямо к итератору (изменяет его прототип, как делает SugarJS), но она далека от завершения, к тому же есть множество более качественных альтернатив: Wu, Lazy.JS и т.д.

Можно написать простенький декоратор который будет это делать для синхронного или для асинхронного итератора. Пример map:
function functor(Target) {
  if(!Reflect.has(Target, Symbol.asycIterator)) throw new Error(`${Target} should be async iterable`);
  if(!Reflect.defineProperty(Target.prototype, 'map', {
    value: async function(transform) {
      const res = [];
      for await(const el of this) {
        res.push(transform(el));
      }
      return res;
    }
  }) throw new Error(`${Target} already has a map method`);
 return Target;
}

@functor // или после объявления класса functor(Queue) если не хочется включать бабель для декоратора
class Queue {...}

В данном примере map просто асинхронно выгребет коллекцию, применит к ней заданную трансформацию и вернет промис с результатом, но можно поведение усложнять и генерировать события на каждый приход элемента.

Лучше уж прототип заменить, чем патчить каждый объект...

Что в коде и делается. Там одна описка при проверке на итератор тоже нужно проверять прототип. А так как раз изменяется прототип класса, а не каждый объект.

А, так вы там класс патчите, а не метод. Да, тогда все правильно.


Хотя можно было бы обойтись просто базовым классом в таком случае.

ну посмотрите, там ведь так и написано декоратор применяется к классу.

нет нельзя, класс может наследоваться от чего-то другого, так что нужен именно декоратор-миксин.
Sign up to leave a comment.

Articles