Javascript-форум (https://javascript.ru/forum/)
-   Node.JS (https://javascript.ru/forum/node-js-io-js/)
-   -   Выполнение массива промисов последовательно (https://javascript.ru/forum/node-js-io-js/74312-vypolnenie-massiva-promisov-posledovatelno.html)

Malleys 05.07.2018 11:07

...тема продолжается
Цитата:

Сообщение от arealhz (Сообщение 488668)
Как можно выполнить массив «промисов» последовательно, т. е. что бы элемент n выполнялся строго после завершения выполнения элемента n-1?

Конструктор «промиса» принимает в качестве аргумента функцию, которая вызывается сразу, когда создаётся объект Promise. Т. е. «промис» запускается сразу, как только создан. Поэтому, если вы создали массив «промисов», то они все сразу запускаются. Даже если вы не передали такой массив в качестве аргумента в функцию `Promise.all`.

Пусть у нас есть «промисы» p1, p2, p3, ... Вот один из вариантов, как их можно запустить последовательно...
[
	() => p1,
	() => p2,
	() => p3,
	() => p4
].reduce((seq, p) => seq.then(p), Promise.resolve());

`p1` запускается сразу, как создан, но функция `() => p1` позволяет отложить запуск до того времени, когда она будет вызвана. Обратите внимание, что метод массива `reduce` возвращает в примере выше «промис», который выполнится, когда будут последовательно выполнены все «промисы» из массива. Так что можно дальше вызывать метод `then`.

Вот ещё один из вариантов, как можно те же «промисы» запустить последовательно...
(async () => [
	await p1,
	await p2,
	await p3,
	await p4
])();

Этот вызов тоже возвращает «промис», который выполнится, когда будут последовательно выполнены все «промисы» из массива...

Цитата:

Сообщение от arealhz (Сообщение 488668)
Так же интересны варианты выполнения массивов «промисов» [...] по 10 за раз

Как вариант использовать итератор...
function* getTasks() {
	const a = Array.from(Array(66), x => 1400 * Math.random() + 100);

	for(let i = 0, len = a.length; i < len; i+=10)
		yield Promise.all(a.slice(i, i+ 10).map(delay));

	function delay(ms) {
		return new Promise(r => {
			setTimeout(() => {
				r(`done: ${ms}ms`);
			}, ms);
		});
	}
}

(async() => {
	for await(const task of getTasks()) {
		console.log(task);
	}
	console.log("all done");
})();


Цитата:

Сообщение от arealhz (Сообщение 488680)
...магия вне Хогвартса.

Просто сравните два примера, оба выражения обозначают одно и тоже, когда вычислены представляют «промис» — пустой контейнер.
(async function() {
    const response = await fetch("https://yesno.wtf/api");
    const { answer } = await response.json();
    console.log(`Ответ ${answer} без магии`);
})();

fetch("https://yesno.wtf/api")
	.then(response => response.json())
	.then(({answer}) => console.log(`Ответ ${answer} без магии`))
;


Ваша конструкция из поста №12 без async/await... они не индентичны в плане трансформации кода, он просто переписан так, чтобы вывод был одинаковый...
"use strict";
'esversion: 6';
function procData(val, i){
	return new Promise(resolve => setTimeout(() => {
		var ret = i + "\t" + val + " #";
		console.log(ret);
		resolve(ret);
	}, 3000));
}
 
[3, 5, "asdf", 8, 0]
	.reduce((seq, item, n) => seq
		.then(retArr => procData(item, n)
		.then(x => retArr.concat(x))
	), Promise.resolve([]))
	.then(console.log);


Ещё примеры async/await, оба вычисленных выражения обозначают одно и тоже...
(async function() { return 5 })();
Promise.resolve(5);


(async function() { throw "Упс!" })();

Promise.reject("Упс!");


Цитата:

Сообщение от arealhz
Оно работает, но я по прежнему не понимаю, почему все работает внутри безымянной функции с пометкой async, которая ещё и сразу вызывается, после объявления.

Давайте тогда не вызывать... `async function() { return 5; }` пример литерала для экземпляра класса AsyncFunction, который вычисляется также как `new AsyncFunction("return 5;")`, этот объект наследует от `Function`.

Такой объект содержит список выражений и объявлений, который выполняется при вызове функции, т. е. как обычная функция, однако ещё имеется выражение с ключевым словом await, например `await b;`. Давайте рассмотрим, как это действует.

Пусть у нас есть блок тела из обычной функций типа `Function` cо следующими выражениями: () => { a; b; c; return d; }
Когда фукция будет вызвана, сначала вычислится a затем b, c, d. Вычисленное d будет результатом вычисления функции. Порядок вычисления всегда линейный и все понимают как это работает.

Теперь рассмотрим блок тела из асинхронной функций типа `AsyncFunction` cо следующими выражениями: async () => { a; await b; c; return d; } Давайте его вычислим. Здесь действуют такие правила(только для вычисления списка выражении):

1. для самого левого выражения с меткой await внутри блока с меткой async:
	a. произведи следующую замену:
		i. A = await B; <всё-до-конца-блока> --> return B.then(async A => { <всё-до-конца-блока> }) если подходит
		ii. await B; <всё-до-конца-блока> --> return B.then(async () => { <всё-до-конца-блока> })
		iii. B --> Promise.resolve(B) если B не «промис»
    b. если ещё есть await см. п. 1
2. для всех блоков с меткой async:
	a. убери async
	b. return A --> return Promise.resolve(A) если A не «промис»
	c. throw A --> return Promise.reject(A)
	d. добавь в конец блока return Promise.resolve() если нет return


получается так(пусть b «промис»):
async () => { a; await b; c; return d; } // 1a
async () => { a; return b.then(async () => { c; return d; }); } //2a
() => { a; return b.then(async () => { c; return d; }); } //2a
() => { a; return b.then(() => { c; return d; }); } //2b
() => { a; return b.then(() => { c; return Promise.resolve(d); }); } // конечный результат


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

Давайте ещё один пример вычислим, который я приводил выше...
(async function() { throw "Упс!" })(); //2a
(function() { throw "Упс!" })(); //2c
(function() { return Promise.reject("Упс!"); })(); // и ещё можно сделать вызов
Promise.reject("Упс!"); // конечный результат


Из этих преобразовании видно, что асинхронная функция всегда возвращает «промис»

Цитата:

Сообщение от Aetae
Promise же в свою очередь - это не что-то самостоятельное, а просто удобная обёртка над коллбэками

Асинхронные коллбэки это объекты типа AsyncFunction, а вот их вызов как раз таки интересен тем, что они выдают удобную обёртку над возвращаемым значением. Это обобщённая на все типы кроме Promise обёртка. При попытке создать обёртку уже на существующей обёртке, мы получаем интересный эффект — ту же самую обёртку по типу. Означает ли это, что в JavaScript'е не существует схлапывания «промиса»?

«Промисы» обладают интересными свойствами. Обозначим p = какой-либо «промис», id = Promise.resolve.bind(Promise) (типа A => Promise<A>), f, g и h какие-либо функция (типа A => Promise<B>). Тогда выполняется:

p.then(id).then(f) === p.then(f).then(id) === p.then(f)
p.then(f).then(x => g(x).then(h)) === p.then(x => f(x).then(g)).then(h) === p.then(f).then(g).then(h)

p.then(f).then(g) === p.then(x => f(x).then(g))


Для каждой пары Promise<A> и Promise<B> задана операция cвязывания `then`
Для Promise<A> задано тождественное cвязывание `Promise.resolve`

«Промис» не является цепочкой вызовов. Объект «промис» является контейнером над значением, которое может быть доступно в данный конкретный момент времени, или, возможно, будет доступно только в будущем.

#funfact

Категории включают объекты и морфизмы. Последние представляют собой вещи и способы перехода от одной вещи к другой. В асинхронных вычислениях JavaScript'а под такими объектами обычно понимаются «промисы», а под морфизмами — методы then, catch и finally, которые переводят состояние одного «промиса» в состояние другого «промиса».

Вопросы для исследования:

Возможно ли получить мгновенное значение промиса p? Если написать `p.then(console.log);`, а промис ещё не выполнился, то мы ничего не получим. Возможно ли мгновенно узнать состояние промиса?

Возможно ли, чтобы промис разрешился другим промисом? Означает ли это, что не существует схлапывания «промиса»?

Возможна ли передача состояния между промисами? Представте себе контейнер, назовём его Writer, у которого есть лог в который можно писать и передавать его между промисами, а также возвращаемое значение. Как бы вы такое реализовали?
// набросок класса
class Writer extends Promise {
	then() {
	}

	runWriter() {
		return "";
	}

	static tell(log) {
		return new Writer(r => r([null, log]));
	}

	static resolve(value) {
		return new Writer(r => r([value, ""]));
	}
}

// пример тестируемой функции
function half(v) {
	return Writer.tell(`I just halfed ${v}!`).then(_=> v / 2);
}

// тестирование в консоли
// > half(8).then(half);
// < Promise { <resolved>: [2, "I just halfed 8!I just halfed 4!"] }
// > await half(8).then(half);
// < 2
// > half(8).then(half).runWriter();
// < "I just halfed 8!I just halfed 4!"
// >

Alexandroppolus 05.07.2018 14:34

Цитата:

Сообщение от Malleys
function Success(v) { return Promise.resolve(v); }
function Failure(v) { return Promise.reject (v); }
function Try(fn) {
    return new Promise(r => r(fn()))
        .then(Success)
        .catch(Failure);
}

это вроде бы можно сократить до
function Try(fn) {
    return new Promise(r => r(fn()));
}


Цитата:

Сообщение от Malleys
Возможно ли получить мгновенное значение промиса p? Если написать `p.then(console.log);`, а промис ещё не выполнился, то мы ничего не получим. Возможно ли мгновенно узнать состояние промиса?

в общем случае нельзя.
если это наш промис (созданный через new Promise(...)), то в его функцию можно дописать изменение какого-то внешнего флажка, и смотреть этот флажок. А иначе никак - даже если промис уже зарезолвлен, функция в then пойдет следующим микротаском, т.е. синхронно не определить. Можно только асинхронно выяснить, что на момент начала проверки промис "уже был зарезолвлен" :)

Цитата:

Сообщение от Malleys
Возможно ли, чтобы промис разрешился другим промисом?

зареджектиться можно, зарезолвиться нельзя, будет вызван его метод then для получения результата. Причем это не только для промиса, а вообще для любого thenable

Aetae 05.07.2018 15:46

Во втором случе можно такой хак зафигачить)
var p = new Promise(e=>e(1));
Object.defineProperty(p, 'then', {
  get(){
    delete p.then;
    return; //первый раз возвращаем undefined и обманываем систему :)
  },
  enumerable: true,
  configurable: true
})

new Promise(e=>e(p)).then(res => alert(res instanceof Promise) ); //возвращает промис :)

В первом случае тоже можно свою обёртку над стандартным промисом прилепить, которая state будет запоминать.

Но зачем?)
Вообще, имхо, слишком мудрите вы с этим.

Alexandroppolus 05.07.2018 16:15

Цитата:

Сообщение от Aetae
Но зачем?)

сказано же - "Вопросы для исследования" :)

Цитата:

Сообщение от Aetae
В первом случае тоже можно свою обёртку над стандартным промисом прилепить, которая state будет запоминать.

и как это сделать?
var oldPromise = Promise;
window.Promise = function(f) {
  console.log('promise');
  return new oldPromise(f);
};
в хроме тот же fetch не подхватывает этот новый Promise

проверку можно сделать, но только асинхронно
function getPromiseState(p) {
	var state = 'pending';
	p.then(function() { state = 'resolved'; }, function() { state = 'rejected'; });
	return Promise.resolve().then(function() { return state; });
}

// --------
getPromiseState(promise).then(console.log);

Aetae 05.07.2018 18:16

Alexandroppolus, концептуально как-то так:
(function(prototype){
  const then = prototype.then;
  prototype.state = 'pending';
  prototype.then = function(){
    const next = then.apply(
      then.call(
        this,
        resolve => {
          this.state = 'fulfilled';
          return resolve;
        }, 
        reject => {
          this.state = 'rejected';
          throw reject;
        }
      ), 
      arguments
    );
    return next;
  }
}(Promise.prototype));

+для Promise.resolve/reject надо перегрузку, ешё мб что-то с catch и final.
+отдельно обработку для случаев когда promise возвращает не promise(передавать в таких случаях статус с this на next, ибо оный в любом случае не поменяется)
+ ...

Alexandroppolus 05.07.2018 18:43

Aetae,
state меняется только после вызова then, и то не сразу

Aetae 05.07.2018 19:20

Alexandroppolus, и? Должно быть как-то по другому?)

Alexandroppolus 05.07.2018 19:42

Aetae,
ну вот, например, я открыл консоль на этой странице, выполнил твой код, а потом:
> prom = fetch('/')
< Promise {<pending>}

> prom
< Promise {<resolved>: Response}

> prom.state
< "pending"

и немножечко смущает, что вот так. Думаю, Malleys спрашивал про то что в треугольных скобках

Aetae 05.07.2018 23:32

Alexandroppolus, ну без then в промисе смысла особого нет.)
А так просто придётся подменять каждую нужную нам нативную фигню возвращающую промис, т.к. все они хранят в себе ссылку на нативный промис, который нам не подменить. Возможно можно как-то поиграться с Symbol.species, но тут уж хз.

Malleys 07.07.2018 09:27

Цитата:

Возможно ли получить мгновенное значение промиса p?
Я такое придумал...
function getPromiseValue(promise) {
    return Promise.race([
        promise, Promise.resolve()
    ]);
}

// > var p = fetch("/");
//   getPromiseValue(p);
// < Promise { <resolved>: undefined }
// > getPromiseValue(p);
// < Promise { <resolved>: Response }
// >


Часовой пояс GMT +3, время: 21:25.