Запрос примеров асинхронных генераторов, которые не могут быть преобразованы напрямую в асинхронную итерацию, реализованную вручную
Асинхронные генераторы используют внутреннюю очередь для обработки синхронных вызовов методов next, throw и return.
Я пытался построить ситуацию, когда эта очередь обязательна для успеха самой итерации. Поэтому я ищу некоторые случаи, когда ручной реализации асинхронных итерационных интерфейсов без пользовательской переопределения очереди недостаточно.
Ниже приведен пример, но не очень хороший, потому что общая согласованность времени не поддерживается, но результат итерации является правильным на каждом шаге:
function aItsFactory() {
let i = 1;
return {
async next() {
if(i > 5) return Promise.resolve({ value: void 0, done: true });
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${i++}`).then(x => x.json());
return Promise.resolve({ value: res, done: false });
},
[Symbol.asyncIterator]() {
return this;
}
}
}
const ait = aItsFactory();
// general time consistency is lost, because e.g. the fourth call
// is started with the previous three and it could end before the others.
// But the 'i' state is correctly shared so the fifth call
// is correctly requesting the element number five to the source
// and the last call will correctly receive { done: true }
;(async () => {
ait.next();
ait.next();
ait.next();
ait.next();
console.log(await ait.next()); // { done: false, value: { userId: 1, id: 5, title: ... } }
console.log(await ait.next()); // { done: true, value: undefined }
})();
Можно утверждать, что без правильной очереди сама концепция итерации будет потеряна. Это из-за активных параллельных следующих вызовов.
В любом случае, я хотел бы найти несколько примеров, также тривиальных, которые дают понять, что асинхронные генераторы являются лучшим подходом для создания правильно сформированных асинхронных итераций, чем ручная реализация асинхронных итерационных интерфейсов.
------ Редактировать ------
Давайте поговорим об улучшении ситуации:
function aItsFactory() {
let i = 1;
let done = false;
return {
async next() {
if (done) return Promise.resolve({
done: true,
value: undefined
});
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${i++}`).then(x => x.json());
if (Object.keys(res).length === 0) { // the jsonplaceholder source is out of bounds
done = true;
return Promise.resolve({
done: true,
value: undefined
});
} else {
return Promise.resolve({
done: false,
value: res
});
};
},
[Symbol.asyncIterator]() {
return this;
}
}
}
const ait = aItsFactory();
// now lot of sync call to 'ait.next'
Здесь done
разрешение полностью асинхронное. С точки зрения асинхронной итерации, код неправильный, потому что каждый next
вызов должен быть вынужден await
исход предыдущего, чтобы знать, была ли это последняя действительная итерация. В таком случае текущий next
ничего не надо делать, сразу возвращаясь Promise.resolve({done:true, value:undefined})
, Это возможно только благодаря очереди синхронизации next
звонки.
Но на практике основной риск выхода за пределы, вызов ait.next()
несколько раз, бесполезный запрос AJAX. Не поймите меня неправильно, я не говорю, что мы можем закрыть глаза. Дело в том, что каждый шаг самой асинхронной итерации никогда не будет нарушен.
Я хотел бы видеть ситуацию, не слишком нереальную, где сама итерация может быть скомпрометирована на каждом шаге, если все последующие вызовы не ставятся в очередь.
1 ответ
Следующий сценарий:
У вас есть поток наборов данных, поступающих, например, из какого-то API. Вы хотите выполнить тяжелые вычисления для каждого набора данных, поэтому вы отправляете набор данных другому исполнителю. Но иногда API может отправлять несколько наборов данных одновременно, и вы не хотите, чтобы одновременно работало много воркеров, вместо этого вы хотите иметь ограниченное количество воркеров. В этом наборе данных вы ищете конкретный результат. С помощью асинхронных итераторов вы можете записать это как:
const incoming = createSomeAsyncIterator();
async function processData() {
let done, value;
while(!done) {
({ done, value } = await incoming.next());
if(!done) {
const result = await searchInWorker(value);
if(result) {
incoming.return();
return result;
}
}
}
}
// Consume tasks in two workers.
Promise.race([
processData(), processData()
]).then(gold => /*...*/);
Приведенный выше код завершится ошибкой, если .next()
не будет возвращать наборы данных по порядку. Тогда кто-то из рабочих может еще продолжить, хотя поиски уже ведутся. Или два воркера могут работать с одним и тем же набором данных.
Или пример ограничения скорости (украденный у Берги:)):
async function* rateLimit(limit, time) {
let count = 0;
while(true) {
if(count++ >= limit) {
await delay(time);
count = 0;
}
yield; // run api call
}
}
const userAPIRate = rateLimit(10, 1000);
async function getUser(id) {
await userAPIRate.next();
return doCall("/user/", id);
}
Или представьте, что вы хотите показать поток изображений в виде галереи (в React):
const images = streamOfImages();
const Image = () => {
const [image, setImage] = useState(null);
useEffect((async ( ) => {
if(image) await delay(10000); // show image at least 10secs
const { value } = await images.next();
setImage(value);
}, [image]);
return <img src={image || "loading.png"} />;
};
const Gallery = () => <div>
<Image /> <Image /> <Image />
</div>;
И еще один, распределение данных по рабочему процессу, чтобы запускался один процесс за раз:
const worker = (async function* () {
let task;
while(true) task = yield task && await doInWorker(task);
})();
worker.next();
worker.next("task 1").then(taskOne => ...);
worker.next("task 2").then(taskTwo => ...);