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)

arealhz 30.06.2018 17:30

Выполнение массива промисов последовательно
 
Добрый день.
У меня есть промис, который обращается некоторое количество раз к базе данных, производит вычисления и потом при резолве возвращает результат (какую-то структуру). При этом этот промис на входе получает определенную структуру данных из большого массива. Все эти промисы создаются в цикле перебирающем массив входных данных. Сами промисы при создании добавляются в массив. После формирования этого массива я жду Promise.all() и получаю массив результатов. Всё бы хорошо. Но вычисления в зависимости от результатов запросов внутри промиса могут быть разные по протяженности, а количество одновременных подключений к базе данных для решения этих промисов ограниченно. И все прекрасно работает, когда в массиве исходных данных 10-20 элементов. А когда там около 500-1000 элементов, то где-то в середине количество одновременных подключений уже вырастает до недопустимого, и база данных перестаёт быть доступной для большей половины промисов. При этом Promise.all уже не выполняется успешно, так как вылазят ошибки в дочерних промисах.

Теперь внимаение впорос :)

Как можно выполнить массив промисов ПОСЛЕДОВАТЕЛЬНО, то есть что бы элемент n выполнялся строго после завершения выполнения элемента n-1?

P.S.: Так же интересны варианты выполнения массивов промисов секторно, на пример по 10шт за раз (что бы эффективно использовать пул подключений к базе данных на пример)

destus 30.06.2018 19:28

Обычный цикл for с использованием внутри себя await. Если нужно несколько промисов за раз, то соответственно увеличение переменной-счетчик на нужное значение и Promise.all() в теле цикла.

Белый шум 30.06.2018 19:38

По-моему вам нужна обёртка над запросами к БД, которая бы следила за числом подключений к базе.

arealhz 30.06.2018 20:20

Я пробовал вот такую модель:

"use strict";
'esversion: 6';


async function procData(val, i){
	await setTimeout(() => {
		var ret = i + "\t" + val + " #";
        console.log(ret);
      }, 3000);
//	return ret;
}


var arr = [3, 5, "asdf", 8, 0];

//arr.forEach(procData(val,i));

//let i = -5;
for(let n in arr){
	procData(arr[n],n)
//	i++;
}


Я понимаю, что здесь я дожидаюсь исполнения конкретного запроса в промисе, но как это ожидание переместить внутрь цикла формирования промисов не совсем понятно. (

Но ничего не вышло. После долгих лет PERL трудно перейти на асинхронную модель программирования. :(
Может я не правильно реализовал async/await?

arealhz 30.06.2018 20:23

Цитата:

Сообщение от Белый шум (Сообщение 488673)
По-моему вам нужна обёртка над запросами к БД, которая бы следила за числом подключений к базе.

Я "сильно новичок" у меня не тот уровень, что бы писать воркароунд для баз данных. Я постигаю асинхронную модель программирования после долгих лет работы с последовательным выполнением кода. И писал всегда утилиты для администрирования, так как я не программист, а админ :)

Aetae 30.06.2018 20:30

1. setTimeout - не возвращает промис, его бесполезно await.
2. ключевое слово await имеет смысл использовать именно там, где ты что-то ждёшь.(а конкретно исключительно перед промисом или вызовом async функции) Если async функция вызвана без этого ключевого слова - то она просто вызвана, дальнейший код не ждёт асинхронного ответа, независимо от того, что там у ней внутри.

Примерно так должен был выглядеть код:
"use strict";
'esversion: 6';
(async function(){

  function procData(val, i){
    return new Promise((resolve, reject) => setTimeout(() => {
      var ret = i + "\t" + val + " #";
      console.log(ret);
      resolve(ret)
    }, 3000));
  }


  var arr = [3, 5, "asdf", 8, 0];

  for(let n in arr){
    await procData(arr[n],n)
  }

}())

arealhz 30.06.2018 21:04

Цитата:

Сообщение от Aetae (Сообщение 488678)
1. setTimeout - не возвращает промис, его бесполезно await.
2. ключевое слово await имеет смысл использовать именно там, где ты что-то ждёшь.(а конкретно исключительно перед промисом или вызовом async функции) Если async функция вызвана без этого ключевого слова - то она просто вызвана, дальнейший код не ждёт асинхронного ответа, независимо от того, что там у ней внутри.

Примерно так должен был выглядеть код:
"use strict";
'esversion: 6';
(async function(){

  function procData(val, i){
    return new Promise((resolve, reject) => setTimeout(() => {
      var ret = i + "\t" + val + " #";
      console.log(ret);
      resolve(ret)
    }, 3000));
  }


  var arr = [3, 5, "asdf", 8, 0];

  for(let n in arr){
    await procData(arr[n],n)
  }

}())

Это действительно работает, но сам код для меня похож на магию вне Хогвартса. :(

Сейчас буду пытаться разобрать этот код, так как без понимания что происходит - использовать вряд ли получиться.

Спасибо большое за пример работающей конструкции! Буду теперь RTFM...

Aetae 30.06.2018 21:46

Чтоб в этом разбираться, надо просто понимать, что async и await - это не что-то самостоятельное, а просто удобная обёртка над Promise. Promise же в свою очередь - это не что-то самостоятельное, а просто удобная обёртка над коллбэками.)
Поняв коллбэки поймёшь и остальное.)

Alexandroppolus 01.07.2018 00:45

Цитата:

Сообщение от arealhz (Сообщение 488668)
P.S.: Так же интересны варианты выполнения массивов промисов секторно, на пример по 10шт за раз (что бы эффективно использовать пул подключений к базе данных на пример)

Самый правильный вариант - "общая очередь". Запускаются параллельно 10 исполнителей, каждый из которых последовательно забирает и обрабатывает элементы из очереди и резолвится, если очередь опустела. А внешний Promise.all ждёт всех десятерых.

Главный плюс - если какой-то пункт обрабатывается долго, он не тормозит остальных.

Audaxviator 01.07.2018 04:52

Посмотрите старую добрую библиотеку async, там 100500 методов на все вкусы и случаи жизни.

Alexandroppolus 01.07.2018 09:50

В async есть parallelLimit, но тут писать всего несколько строк кода

общая очередь делается примерно так:

// последовательная обработка очереди
function runWorker(func, getNext) {
	return new Promise(function (resolve) {
		(function runItem() {
			var item = getNext();
			if (item == null) {
				resolve();
			} else {
				func(item).then(runItem, runItem);
			}
		})();
	});
}

// использование

// функция, которая что-то делает асинхронно
var func = function(a) {
	var w = this;
    console.log('worker ' + w + ': start ' + a);
    return new Promise(function(r) {
         setTimeout(function() { console.log('worker ' + w + ': end ', a); r(); }, a * 1000);
    });
};

// очередь параметров
var data = [4, 3, 8, 2, 5, 6, 12];
var getNext = data.shift.bind(data);

console.log('queue: ' + data.join(' '));

// два параллельных потока
Promise.all([
    runWorker(func.bind(0), getNext),
    runWorker(func.bind(1), getNext)
]).then(console.log.bind(console, 'end all'));


если это выполнить с открытой консолью, то можно увидеть, каким работником какой пункт обрабатывается. "Свободная касса!" и всё такое

arealhz 01.07.2018 11:53

Цитата:

Сообщение от Aetae (Сообщение 488678)
1. setTimeout - не возвращает промис, его бесполезно await.
2. ключевое слово await имеет смысл использовать именно там, где ты что-то ждёшь.(а конкретно исключительно перед промисом или вызовом async функции) Если async функция вызвана без этого ключевого слова - то она просто вызвана, дальнейший код не ждёт асинхронного ответа, независимо от того, что там у ней внутри.

Примерно так должен был выглядеть код:
"use strict";
'esversion: 6';
(async function(){

  function procData(val, i){
    return new Promise((resolve, reject) => setTimeout(() => {
      var ret = i + "\t" + val + " #";
      console.log(ret);
      resolve(ret)
    }, 3000));
  }


  var arr = [3, 5, "asdf", 8, 0];

  for(let n in arr){
    await procData(arr[n],n)
  }

}())

Сегодня баловался самой констуркцией. Пришел к вот такому рабочему варианту:

"use strict";
'esversion: 6';
(async function(){
 
  function procData(val, i){
    return new Promise((resolve, reject) => setTimeout(() => {
      var ret = i + "\t" + val + " #";
      console.log(ret);
      resolve(ret)
    }, 3000));
  }
 
 
  var arr = [3, 5, "asdf", 8, 0];
  var retArr = [];
 
  for(let n in arr){
    retArr.push(await procData(arr[n],n));
  }
 
  Promise.all(retArr).then((ret)=>{console.log(ret)});
}())


Оно работает, но я по прежнему не понимаю, почему все работает внутри безымянной функции с пометкой async, которая ещё и сразу вызывается, после объявления. Почему нельзя поставить async перед функцией внутри которой создается промис, почему нельзя поставить await перед самим промисом? Одни вопросы. Прочитал несколько статей на хабре, но материал, можно сказать, не усвоил. Но при этом с калбеками вроде более менее разобрался и ужасную лапшу из них строить могу :) А вот промисы и async/await не даются.

arealhz 01.07.2018 11:56

Цитата:

Сообщение от Alexandroppolus (Сообщение 488695)
Самый правильный вариант - "общая очередь". Запускаются параллельно 10 исполнителей, каждый из которых последовательно забирает и обрабатывает элементы из очереди и резолвится, если очередь опустела. А внешний Promise.all ждёт всех десятерых.

Главный плюс - если какой-то пункт обрабатывается долго, он не тормозит остальных.

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

arealhz 01.07.2018 11:58

Цитата:

Сообщение от Audaxviator (Сообщение 488697)
Посмотрите старую добрую библиотеку async, там 100500 методов на все вкусы и случаи жизни.

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

Aetae 01.07.2018 12:14

for(let n in arr){
    retArr.push(await procData(arr[n],n));
  }
 
  Promise.all(retArr).then((ret)=>{console.log(ret)});
Это излишне.
Достаточно либо:
for(let n in arr){
    retArr.push(procData(arr[n],n));
  }
 
  Promise.all(retArr).then((ret)=>{console.log(ret)});
наполняем массив промисами, затем ждём выпонениния всех.
Либо:
for(let n in arr){
    retArr.push(await procData(arr[n],n));
  }
 
  console.log(retArr)
наполняем массив ожидая выполнения каждого промиса по очереди.

Цитата:

Оно работает, но я по прежнему не понимаю, почему все работает внутри безымянной функции с пометкой async, которая ещё и сразу вызывается, после объявления.
Потому что await работает только внутри async функции, и в простом коде ничего await нельзя. Как я сказал выше - это лишь обёртка над промисами, в сам язык вписанная, имхо, не слишком красиво.

Цитата:

Почему нельзя поставить async перед функцией внутри которой создается промис, почему нельзя поставить await перед самим промисом?
Можно. Только если вызывать async функцию не поставив перед самим вызовом await, то ждать её вырлнения код не будет, и, хотя сам асинронный код в функции выполнится, но отдельно, не возвращая результата и не откладывая выполнения остального кода.

Цитата:

Но при этом с калбеками вроде более менее разобрался и ужасную лапшу из них строить могу :) А вот промисы и async/await не даются.
С коллбэков переходите на промисы. Самый постой вариант понять, как это работает - написать свой класс Promise, что абсолютно реализуемо классическими методами. (Если не получается - погуглить "промис полифил" и разобраться что да как)

arealhz 01.07.2018 12:25

Вот это вообще шикарно! Не уверен, что мне нужны отдельные воркеры (что модуль node который я использую не завалиться на них), но шикарно!

arealhz 01.07.2018 12:29

Спасибо за развернутое объяснение. С собственным классом промисов уже пытался. С большим количеством подсказок получилость сделать что-то рабочее, но так как полноценно не тестировал - использовать в собственном проекте не стал. У меня в части моих модулей модуль под названием promise, а в части bluebird :)
До сих пор не могу определиться что оставить )))

Alexandroppolus 01.07.2018 15:32

Цитата:

Сообщение от arealhz
я работаю с сервисом, который уже имеет некоторую настраиваемую многопоточность запросов, соединяюсь с ним через готовый модуль, который вроде имеет пул соединений (но это не точно - надо перепроверить), но всё равно я объемом тестовых данных кладу эту структуру.

значит, там нет такого механизма, который бы умел работать с очередью. Не предусмотрен лимит количества одновременно выполняемых запросов. Точнее, предусмотрен, только в виде падения системы :)

хотя, не исключаю, что он там есть, только надо его как-то задействовать.

stweet 02.07.2018 09:31

Вот, как то тоже боролся с подобной задачей. - но тут есть нюанс, обработку эксепшенов берите на себя (я же делал промежуточный обработчик - некий виртуальный слушатель). Ещё это можно решить рекурсивно и при падении (эксепш в цикле) перезапускать рекурсию с индексом падения. Такие помощники как Promise.all() не помогут когда надо "все" обработать в не зависимости от результата. Во время рекурсии результаты можно скидывать в стек, некий диспетчер, будет раздавать ответы по коду. Т.е. вы не ждете ответа от промиса а подписываетесь на событие с неким кодом, промис - когда выполниться сообщит вам о завершении через диспетчера.

Malleys 05.07.2018 11:06

Проще всего представлять себе «промис» как контейнер. Не какой-то конкретный, а просто любой контейнер. Подойдёт и просто обёртка для ссылки на объект: это — как бы контейнер, в котором может быть только один элемент.

Для этого контейнера должна быть определена операция, которая на самом деле — просто операция создания контейнера. По понятным причинам она должна быть: иначе их было бы невозможно использовать. Фактически эта операция — статичный метод, создающий этот контейнер — метод `Promise.resolve`.

Ещё должна быть определена операция отображения одного контейнера на другой. Более точней — операция «связывания» одного контейнера с другим. Чтобы понять, почему именно оно, а не что-то другое, рассмотрим сначала операцию, которая чуть проще — метод `map` у массива Array.

Пусть у нас имеется массив с элементами 1, 2 и 3. При помощи метода `map` этого массива мы можем над каждым элементом произвести какую-то операцию и получить новый массив, в котором находятся преобразованные данной операцией элементы. Например, мы можем умножить каждый элемент списка целых чисел на пять.

const a = [1, 2, 3];
const newA = a.map(x => 5 * x);

console.log(newA);
// > [5, 10, 15]


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

Важно то, что на выходе получается контейнер того же класса: был Array — новый тоже будет Array-ем. При этом тип значения в новом контейнере может поменяться. То есть, мы могли бы не умножать на 5 элементы контейнера, а, например, превращать их в строки. Тогда у нас из Array<Number> получился бы Array<String>.

const a = [1, 2, 3];
const newA = a.map(x => `*${x}*`);

console.log(newA);
// > ["*1*", "*2*", "*3*"]


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

Рассмотрим пример: нужен поиск чего-то где-то. Например, первого элемента в массиве строк, который удовлетворяет заданным условиям. Сигнатура функции поиска в этом случае будет примерно такая.

function find(/* Array<String> */ array, /* String => Boolean */ criteria) {}


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

Ну а что возвращать, когда элемент, удовлетворяющий условию, не найден? null?

Ну, в этом примере оно, конечно, более-менее подходит, однако в общем случае null может считаться корректным значением и лежать в массиве. Как его отличить от «ничего не найдено», и, наконец, как потом обрабатывать результат? Ведь можно просто вызвать нужный метод у найденного объекта позабыв о том, что в качестве ответа может вернуться null.

И вот тут в тему оказываются контейнеры с одним элементом, которое может быть не указано, а именно так устроены «промисы»: надо просто результат поиска оборачивать в такой контейнер.

Рассмотрим один из них, назовём его Option. Option имеет два варианта: Some, то есть «какой-то», и None — «ничего». Если мы что-то нашли, то мы возвращаем Some(найденное), если нет — возвращаем None().

class Option extends Promise {}

function Some(v) { return new Option((r, _) => r(v)); }
function None( ) { return new Option((_, r) => r( )); }


Если оборачивать результат операций, подобных поиску или открытию файлов (т. е. те, где можно ничего не найти, неудачно открыть и т. п.) в такие контейнеры, то вышеописанные проблемы устраняются. Исключение при попытке вызова метода у null возникнуть уже не может. Есть, что возвращать даже для примитивных типов, безо всяких там «магических» констант вида «-1 означает, что ничего не найдено», и т. д.

Чтобы понять, в чём прикол такого контейнера, рассмотрим пример с серией операций.

Предположим, что у нас есть некоторый довольно примитивный набор методов для обработки файлов. Файл можно открыть, после этого можно прочитать его содержимое, которое можно «распарсить». И наконец, файл можно закрыть, чтобы высвободить ресурс. Для краткости вместо реализации этих методов я сделаю заглушки, ведь интересует только их использование.

class ParsedData {}

class File {
	read() { return ""; }
	close() {}
}

function openFile(fileName) { return new File }
function parseString(str) { return new ParsedData }


Примерно так выглядит способ работы с таким интерфейсом...

const file = openFile("my-file.txt");
const parsed = parseString(file.read());
file.close();


Всё cупер, кратко и лаконично. Однако есть много проблем. Каждый из этих методов может «не сработать». Файла с таким именем может не быть. Он может быть, но не читаться. Даже прочитавшись, он может содержать синтаксические ошибки и потому не «парситься». И закрытие файла тоже может завершиться с ошибкой.

Традиционный способ решения подразумевает использование try-catch. И вот во что превращаются эти простые три строки в данном случае.

let file, parsed;

try {
	file = openFile("my-file.txt");
	parsed = parseString(file.read());
} catch(error) {
	console.error(error);
} finally {
	try {
		if(file) file.close();
	} catch(error) {
		console.error(error);
	}
}


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

Посмотрим, что нам даст вышерассмотренный контейнер с одним или нулём элементов — Option. Сначала переопределим методы для обработки файлов так, чтобы они не выкидывали исключений, а если возвращают результат, то возвращали бы его обёрнутым в Option.

class ParsedData {}

class File {
	read() { return Some(""); }
	close() { return Some() }
}

function openFile(fileName) { return Some(new File) }
function parseString(str) { return Some(new ParsedData) }


Воспользуемся тем, что Option — это контейнер. Причём он не просто контейнер, а контейнер с методом `then`, поскольку наследует от класса Promise. Это нам даёт следующее:

1. Если в контейнере нет элементов (None), то написанное в `then` не будет выполнено ни разу
2. Если в контейнере один элемент (Some), то написанное в `then` будет выполнено один раз — именно для того, что обёрнуто в этот Some
3. Нам вернётся результат выполнения, обёрнутый в Option: Some(результат), если всё прошло удачно, и None() — если нет.

Это позволяет нам написать вот так...

const file   = openFile("my-file.txt");
const parsed = file.then(f => f.read()).then(parseString);
file.then(f => f.close());


Пусть, с небольшими добавками, но это всё-таки почти те же исходные три строки. Почти столь же хорошо читаемые.

Теперь, что делать, если мы хотим не просто проигнорировать все ошибки, а уведомить о них пользователя?

Идея та же: контейнер с одним элементом. С той лишь разницей, что вместо None должно использоваться что-то типа OtherSome(exception), которая будет, подобно None, «путешествовать» от одной строки к другой и, в конце концов, вернётся к нам, если что-то пойдёт не так.

function OtherSome(exception) { return Promise.reject(exception); }


Ещё «промис» может выполнять роль контейнера Try — это такая версия контейнера с одним элементом, которая внутри себя перехватывает возникшие исключения. Но не просто их игнорирует, а запоминает внутри обёртки, которая трактуется так же, как в Option трактуется None: никаких преобразований совершать не надо, надо просто вернуть то же самое обёрнутое исключение.

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)
	;
}

// пример с вводом целого числа
Try(() => {
	const a = prompt("Insert number");
		if(/^\d+$/.test(a.trim()))
		return a;
	else
		throw "Not a Number";
}).then(console.log, alert);


В результате вышенаписанного в контейнере будет лежать либо Success(правильный ввод), либо Failure(то исключение, когда ввели не целое число).

Также «промис» можно представить контейнером — назовём его Future — над значением, которое может быть доступно в данный конкретный момент времени(resolved), или, возможно, будет доступно только в будущем(pending), или никогда не будет доступно(rejected)
function Future(fn) {
	return new Promise(fn);
}


В принципе роль функций Some, None, Success, Failure, Try, Future — вспомогательная, они возвращают «промис», который можно было бы создать напрямую в коде. Однако согласитесь, что наделяют большим смыслом то, что просходит в коде, чем просто Promise.resolve или Promise.reject.

Об отображении

Функция `Promise.prototype.then` работает так...
из объекта типа Promise<A>                    // обозначим его как p1
и функции типа (A => Promise<B>) или (A => B) // обозначим её  как f
делает Promise<B>                             // обозначим его как p2
т. е. p1.then(f) === p2


Для сравнения, функция `Array.prototype.map` работает так...
из объекта типа Array<A> // обозначим его как p1
и функции типа  (A => B) // обозначим её  как f
делает Array<B>          // обозначим его как p2
т. е. p1.map(f) === p2


Об упаковке в контейнер

Функция `Promise.resolve` работает так...
из объекта типа A // обозначим его как a
делает Promise<A> // обозначим его как p
т. е. Promise.resolve(a) === p


Для сравнения, функция `Array.of` работает так...
из объекта типа A // обозначим его как a
делает Array<A>   // обозначим его как p
т. е. Array.of(a) === p


Продолжение скоро...

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 }
// >

Aetae 07.07.2018 12:45

Malleys, ты это только в консоли и видишь, для этого вообще не надо ничего делать)


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