Pull to refresh

Comments 182

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

> Мне кажется, что комитет слишком увлекся синтаксическими плюшками
Нет. На ES6+ писать намного приятней, чем на ES5. Порог вхождения, правда, существенно выше

> Проблемы с left-pad можно было легко избежать, если бы этот метод был в стандартной библиотеке
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart

Да? И как там вывести дату в формате DD.MM.YYYY?

Очень просто,
Date().replace(/(.+?)(\w{3}) (\d+) (\d{4})(.+)/,'$3.$2.$4')
ну и там строчку в число перевести массивом немного: )

Это называется не «очень просто», а «жуткий костыль». Вот просто:


moment().format('DD.MM.YYYY');

Сарказм-детектор проверьте, кажется он у вас поломался

let date = new Date();

date.toLocaleString('ru-RU', { day: 'numeric', month: 'numeric', year: 'numeric' });

Офигенное API. Но вроде бы работает, справедливости ради.

Можно еще короче:

new Intl.DateTimeFormat('ru').format(new Date());
И ещё: Intl.DateTimeFormat().format(new Date)

Да, и вот как без неё:
var dateFormat = ()=> Date().replace(/.+?(\w\w) (\d+) (\d+).+/,(a,i,j,k)=>j+'.'
	+('anebarprayunulugepctovec'.indexOf(i)/2+101+'').substr(1)+'.'+k);

Видите ли в чем дело — поставить сторонний пакет проще чем "поставить" синтаксическую плюшку. Поэтому синтаксичкские плюшки — это в языке самое важное.

Для работы с датами стандартом de-facto стал moment.js, несмотря на корявость своего API (особенно его любовь мутировать даты). Меня уже давно мучает вопрос, почему просто не взять и не включить его как стандарт? Доработать API и красота.

Не уверен, что одобряю эту идею. Помнится, были предложения включать jQuery прямо в состав браузера — мол, все равно 95% сайтов его использует. Никто не стал этого делать, и слава богу.

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

Разве?

Видимо, имелась в виду вспомогательная функция, видимая в инструментах разработчика.

А также classList, map, each, Promises, formData и fetch, css анимации, да в общем-то почти все в том или ином виде застандартизировали, и слава богу, что не под копирку, только про цепочки вызовов забыли.

В конструкции then не надо оборачивать в новый промис c then, поскольку return возвращает промис, надо просто продолжить цепочку. async/await конечно кросиво и удобно но не дает какой-то панацеи и в некоторых случаях промисы очень удобны. Взять хотябы Promise.all(). Так что не понимаю зачем делать категоричные заявления об отказах от текущего функционала, если можно просто всё использовать в нужных мустах
К слову в C# есть await Task.WhenAll(awaitable1, awaitable2). Как я понимаю если Promise.all() возвращает промис, То можно сделать то же самое?
Определенно можно. Когда в C# появился синтаксис async/await, у меня лично весь асинхронный код стал гораздо симпатичнее и понятнее. Дичайше обрадовался тому же синтаксису в JS, они молодцы что выбрали именно C# в качестве примера для подражания.
Спасибо. Только начинаем слезать с ES5 и еще не успел все пощупать :)
Я к тому, что нет await all, да и async это лишь синтаксический сахар для промисов, так что про них не стоит забывать. Но вот на счет исключений в async/await я полностью согласен с автором, гораздо удобнее

Зачем нужег await all если можно написать await Promise.all(...)?

Изначально, кстати, предлагался вариант:

async function concurrent () {
  var [r1, r2, r3] = await* [p1, p2, p3];
}

… но он был отклонён.
async это лишь синтаксический сахар для промисов

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

async это лишь синтаксический сахар для промисов
нечто большее, чем синтаксический сахар.
Я в кишках V8 не копался, но у меня есть стойкое подозрение, что async/await работает поверх генераторов с резолвом за-yield-енного промиса при прогоне результирующего итератора. Результат .catch же скармливается .throw. Слишком уж «гибкий» для промисов флоу получается, на них такого без бубнов не сделать, взять те же циклы.
Посмотрите, во что бабель транспайлит async/await с включенным transform-async-to-generator. Ну очень похоже, что V8 делает то же самое, при чем на том же JS, как в случае с промисами.
Если я перечитал этот коммент два раза и всё равно не понял, мне ещё можно быть веб-девелопером или пора уходить в домохозяйки?
Постараюсь подробнее. Возьмем небольшой пример, сдобренный async/await:
//исходник с разными вариантами async/await
const delay = ms => new Promise(resolve => {
  setTimeout(() => resolve(ms), ms);
});

const throwAsync = (error, ms) => new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error(error)), ms);
})

const foo = async (n, shouldThrow) => {
  for (let i = 1; i < n + 1; i++) {
    const result = await delay(i * 1000);
    console.log('wait', result);
  }  
  
  if (shouldThrow) {
    await throwAsync('Should throw', 1000);
  }
  
  return 'done';
}

foo(3).then(::console.log).catch(::console.error);
foo(3, true).then(::console.log).catch(::console.error);

//результат после бабеля с transform-async-to-generator
var _context;

function _asyncToGenerator(fn) {
    return function() {
        var gen = fn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            function step(key, arg) {
                try {
                    var info = gen[key](arg);
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }
                if (info.done) {
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(function(value) {
                        step("next", value);
                    }, function(err) {
                        step("throw", err);
                    });
                }
            }
            return step("next");
        });
    };
}

const delay = ms => new Promise(resolve => {
    setTimeout(() => resolve(ms), ms);
});

const throwAsync = (error, ms) => new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(error)), ms);
});

const foo = (() => {
    var _ref = _asyncToGenerator(function*(n, shouldThrow) {
        for (let i = 1; i < n + 1; i++) {
            const result = yield delay(i * 1000);
            console.log('wait', result);
        }

        if (shouldThrow) {
            yield throwAsync('Should throw', 1000);
        }

        return 'done';
    });

    return function foo(_x, _x2) {
        return _ref.apply(this, arguments);
    };
})();

foo(3).then((_context = console).log.bind(_context)).catch((_context = console).error.bind(_context));
foo(3, true).then((_context = console).log.bind(_context)).catch((_context = console).error.bind(_context));

Уделите внимание функции _asyncToGenerator, как она работает с промисами, принимаемыми из step("next"). Еще посмотрите, как все await заменились на yield абсолютно в тех же местах без перекомпоновки кода, что пришлось бы делать при переписывании на промисы.

Исходный посыл был к тому, что есть очень большие подозрения, что async/await внутри V8 сделан не на основе промисов, а как раз на основе генераторов.

Поиграться можно вот с таким набором пресетов.
Исходный посыл был к тому, что есть очень большие подозрения, что async/await внутри V8 сделан не на основе промисов, а как раз на основе генераторов.

Почему вы разделяете эти два случая?

Не разделяю, а дополняю следующее:
async это лишь синтаксический сахар для промисов
Действительно, промисы и генераторы работают в тандеме в случае async/await. Async функция — это возвращаемый промис, а await точки — yield в генераторе.
Отсюда и древний холивар, что async/await не нужен, так как все то же самое можно сделать на генераторах и маленькой функции-хелпере.

Да, но зато можно определить через символы является ли функция async:


var fn = async function () {}; 
fn[Symbol.toStringTag] === "AsyncFunction";

Ну и в чём прикол? В том что они реализованы на уровне движка js? Какая разница, они все реализованы одинаково по смыслу, просто так они реализованы в движке, а так их нужно реализовывать самим.

И зачем вам это, скажите, пожалуйста?
Через символы можно много чего определить, в том числе и генераторы:
(function * foo() {})[Symbol.toStringTag] === 'GeneratorFunction'

Другое дело, что смысловой нагрузки это никакой не несет.
Это все, конечно, прекрасно, только из вашего примера вытекает только то, что async/await существует для того, чтобы его можно было задетектить через Symbol.toStringTag. Генераторы тоже детектятся, а решают те же задачи и даже чуть больше.

И если уж вы используете стороннюю библиотеку, то хелпер для «размотки» генератора есть в co.

Я не пользуюсь именно этой библиотекой, просто пример, где это используется. Сам я буду использовать "AsyncFunction" немного по другому, для определения функций в обертке и отслеживании вывозов API внутри системы (аналитика производительности, мониторинг ошибок).

Нет никакого противоречия. Когда метод в конечный автомат или генератор превращает транслятор — это нормально. Когда то же самое делает программист вручную — это пляски с бубном и костылями.

Нет никакого противоречия.
Хм, я не имел в виду противоречие про сахар, а просто объединил =)

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


Если это требуется для реализации задачи — то почему нет?

Не знаю, как в JS, а в C# за асинхронными функциями стоит довольно суровая кодогенерация. Грубо говоря, каждая функция разворачивается в класс, переменные внутри функции — в поля класса, а ветки выполнения, содержащие await — либо в отдельные функции, либо в функцию с большим switch (можно и так, и так реализовывать машину состояний).


Можно этот класс написать вручную, тогда код с использованием async/await будет идентичен промисам.
Так что async/await — это именно сахар.

async/await раскладывается бабелем при транспайлинге в es5 как раз в свитч вместе с regeneratorRuntime (рантайм который этот свитч перемалывает). Собственно, генераторы раскладываются в похожий свитч с тем же рантаймом.
К сожалению, не очень хорошо разбираюсь в подкапотной работе V8 в оптимизации async/await, но немножко могу про C#, и там компилятор очень умеет оптимизировать код. Скажем, когда результаты функций должны использоваться совместно:

let a = await _a()
let b = await _b()
let c = await _c(a, b)
console.log(c)


В мире с промисами это будет выглядеть как-то вот так:

Promise.all(_a(), _b())
  .then(r => _c(r[0], r[1]))
  .then(console.log)


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

В C# эти страшные «промисы» (Task) резолвит внутренний оптимизатор, по сути оно во что-то такое и превращается, но пользоваться этим гораздо удобнее, имхо.

Нет. Ваш пример с await с обычными промисами выглядел бы так:


_a()
  .then(a => _b().then(b => _c(a, b))
  .then(console.log);

То есть еще хуже, чем вы написали. Сначала выполнится промис _a, и только потом — _b.


Чтобы промисы работали синхронно, действительно нужно использовать Promise.all:


let [a, b] = await Promise.all([_a(), _b()]);
let c = await _c(a, b);
console.log(c);

Необязательно. Можно же и вот так сделать:


let a = _a();
let b = _b();
let c = _c(await a, await b);
console.log(await c);

Но с тем, что первый пример был некорректен — согласен.

Нельзя. Сделаем такой простенький промис для демонстрации:


const  p = (a, b) => new Promise(resolve => setTimeout(resolve, 1000, b ? { a, b } : a));

Вопрос на засыпку: когда в консоли появится результат — через секунду, две или три?


(async () => {
    console.log(await p(await p(1), await p(2)));
})()

Или более развернуто (как у вас):


(async () => {
    let a = p(1);
    let b = p(2);
    let c = p(await a, await b);
    console.log(await c);
})()

Вы изменили мой код! Он должен выглядеть вот так:


(async () => {
    let a = p(1);
    let b = p(2);
    console.log(await p(await a, await b));
})()

Результат в консоли появится через 2 секунды. А приведенный вами код покажет его через 3 секунды.

Вообще, развернутый вариант у меня в комментарии тоже за 2 секунды отработает.


Причины такого поведения понятны, но совершенно (ИМХО) не очевидны. Попробую использовать вопрос о разнице между этими двумя кусками кода и её причинах на собеседованиях =)

Они очевидны и интуитивно понятны любому, кто писал асинхронные программы на c# используя Begin/End пары методов :-)

У меня такие не часто попадаются.

То есть получается, там, где я сказал "нельзя", на самом деле можно. Довольно-таки глупо с моей стороны не заметить, что оба промиса (a и b) запускаются сразу друг за другом.

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

Я не луддит мне просто не очевидно зачем менять один синтаксис на другой.

Второе, все приведенные «плюшки» совсем меркнут если использовать bluebird – он очень улучаешт работу с ошибками и дает кучу утилит для координации промисов.

Императивный код проще чем каллбэк спагетти в любое время дня и ночи. Движение языка в сторону упрощения это офигеть как круто. Обычно они все только усложняются...

промахнулся комментарием – ответил вам ниже
стоп, кто говорил про callback спагетти? предлагают забыть о промисах

тут предлагают использовать async/await и забыть о промисах делая чуть короче простые примеры. Но ничего не сказано про более сложные кейсы – что делать с типизацией ошибок? как координировать несколько промисов? все это решает bluebird

второе, статья манипулятивна:
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {
      // do something          
      return promise3(value1, value2)
    })
}

так выглядит намного короче
const makeRequest = () => promise1()
    .then(value1 => Promise.all([value1, promise2(value1)]))
    .then(([value1, value2]) => promise3(value1, value2))


а с блюбердом совсем так:
const makeRequest = () => promise1()
    .then(value1 => [value1, promise2(value1)])
    .spread((value1, value2) => promise3(value1, value2))


>Движение языка в сторону упрощения
js и так простой, то что с этой «фичей» ассинхронный код начинает выгдлядеть как синхронных – сомнительный плюс.

код ниже не имеет смысла – с тем же успехом можно писать синхроно и не заморачиваться и есть вероятность что даже быстрее будет

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())


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

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

у меня сложилось впечатление что вы путаете callback и promise.

>он им и является, потому что стек сохраняется отсюда все плюсы.
вы уверены в своих словах? в чем профит? юзайте все что *sync из стандартной библиотеки – будет вам счастье.

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

Да, коллбеки (промисы) с исключениями — самая большая у них проблема. Раньше просто async не было, поэтому предлагали меньшее зло, тем более, что того же синтаксиса, но упорно шли к async и чуть раньше — к урожаю: ).

Вам никто не мешает использовать async/await и bluebird вместе.

конечно, но статья называется «Async/await: 6 причин забыть о промисах». Что само по себе уже уже смешно – потому как предлагают забыть о .then и .catch методах промисов.

Вот только вы не учли, что под // do something может оказаться страница кода, которую вы смело выкинули.
ага, вам не показалось странным что в примере с Promise «do something» есть, а в примере с async/await нет?

или вы думаете что «do something» во втором случае будет меньше?
Если располагать тоже количество «логики» на строчку кода, то выйдет что примеры аналогичны по длине
Например:

const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
//против:
const makeRequest = () =>
  getJSON().then(console.log)
    .then(()=> 'done')

async () => {
  let t1 = await call();
  let t2 =  await call();
  let t3 =  await call();
}


Я верно понимаю, что в примере выше call() будет вызываться асинхронно, и не дожидаться завершения предыдущих вызовов?

Нет. Каждый await ожидает завершения прежде чем передать управление дальше.


Чтобы было как вы написали — надо делать так (пишу по памяти, могу напутать с синтаксисом немного):


var [f1, f2, f3] = await Promise.all([call(), call(), call()])
тут важно добавить – что call таки может быть «асинхронным»(например вызвать что-то по сети) и не будет блокировать event-loop(а значит где-то в другом месте что-то может выполнится)

Но сам кусок написан так что call'ы будут выполнятся по очень – если нужно условно одновременно, то нужен Promis.all как mayorvp написал

как вариант без Promise.all


async () => {
  let t1 = call();
  let t2 = call();
  let t3 = call();

  let f1 = await t1;
  let f2 = await t2;
  let f3 = await t3;
}
Что вы хотели показать своим примером?

Очевидно, что то что и написал, код выше эквивалентен этому


var [f1, f2, f3] = await Promise.all([call(), call(), call()])

Не эквивалентен, т.к. try-catch с таким кодом не работает, реально будет поймано исключение только для первого await, остальные уйдут в "Unhandled promise rejection" (с Promise.all такой проблемы не будет). В соседнем топике обсуждалось — https://habrahabr.ru/post/326442/#comment_10177554

Ну так вроде ничто не мешает каждый await обернуть в try-catch, за одно удобнее различные исключения бросать

Последовательно, опять же… А нужно именно паралельное. Допустим t1 будет идти 7 мс, t2 — 3 мс, t3 — 12 мс. Если сделать через Promise.all(), то даже если мы будем учитывать, что на вызов call уходит само по себе 1 мс, получится всего max(12, 7, 3) + 3 = 15 мс, в отличии от ваших последовательных 7 + 1 + 3 + 1 + 12 + 1 = 25 мс. Мне кажется, что преимущество Promise.all() достаточно очевидно.

bano-notit
вы не поверите, но таки пресдтавленный maxxon код «почти»(с оговорками для ноды) паралельный и медленнее того который с Promise.all на ~2ms – и то, есть вероятность, что это лаг, а не реальное замедление

Другой вопрос – что это не очевидно, и синкаксически его код не показывает что он «паралельнный»

Блин… Согласен, не досмотрел. Ведь промисы все просто в переменных сохраняются, а не генерятся на самом await...


Но то, что это совершенно не очевидно — 100%.

await и генерит промисы, как видно из названия он их ждет

Другой вопрос – что это не очевидно, и синкаксически его код не показывает что он «паралельнный»

call() — запуск запроса, а await — ожидания его выполнения. То есть сначала запускается три запроса и только потом запускается их ожидание. Мне было вполне очевидно, что они исполняются параллельно. Видимо, необходимо базово понимать, как этот код работает.

Ну, тут проблема не столько в оператре await, сколько в функции call. По названию не видно что она асинхронная.

А зачем впоминать про статью демагога, которая может вызвать только фейспалм?

У меня вызывают подобную реакцию ваши оценочные суждения.

Ваше сообщение сообщение имело б смысл, Если бы там в комментариях не было аргументов того, что статья фиговая.

Вопрос такой: как с помощью async/await организовать тот же самый Promise.all()? Ведь в таком коде


let a = await first(),
    b = await second();

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


let [a, b] = await Promise.all([first(), second()]).then((a, b)=> [a, b]);

А это как-то не сильно входит в идею синтаксического сахорочка...

Как показали тесты .then() в данном случае не является нужным… Но всё равно конструкция получается не сильно сладкая.

Вот тут ответили:
https://habrahabr.ru/company/ruvds/blog/326074/#comment_10164346

Но, честно говоря, с Promise.all как-то интуитивней становится. А то слишком замороченно получается.

Да вообще-то можно было бы вообще для всего промис интерфейса сделать сахарок, например логические операторы для промисов) Это было бы вообще офигительно нечитаемо.

ну не знаю, по-моему вполне читаемо:
let [a, b] = await [ first(), second() ];

Не согласен. потому что по идее эта конструкция должна выглядеть так:


let [a, b] = [await first(), await second()];

Ведь массив не является промисом… А в качестве синтаксического сахарка принимать массивы…
Ну это получается нужно ещё и такую вещь реализовывать:


let {first: a, second: b} = await {first: first(), second: second()}

Да и что тогда делать, например с такой вещью?


let [a, b] = await [1, second()]

Ведь такое вполне может произойти… Promise.all() с этим справляется, но вот реализовать такое на синтаксическом сахаре будет уже много менее явно и понятно со стороны.

let {first: a, second: b} = await {first: first(), second: second()}

Это валидно, остальное пусть валится с ошибкой.

Хорошо так придумано! То есть Promise.all() может, а синтаксический сахар к нему пусть с ошибкой валится...

Да, тут вы правы. Интерфейс стоит как у all сделать.

Тогда без объектов… Тогда не весь изначальный синтаксический сахар присвоения значений получается… Короче говоря дилемма.

Понимаете ли, в этом то и прикол, что нужно либо добавлять объекты внутрь Promise.all(), что не сильно влечёт за собой проблемы, либо не делать этой прикольной штуки для объектов, либо сделать ещё умнее, сделать await отдельным оператором, который принимает только промисы, а остальные значения возвращает через Promise.reslove(), как собственно и сделали они)

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

На счёт поддержки старого у меня почему-то вспоминается сразу ie… Не знаю почему, но ассоциация у меня прям жёсткая, когда слышу/читаю слово "совместимость"…
А на счёт того, что должны или нет, тк вопрос в том, что js сам по себе начинает поддерживаться по кускам, поэтому сейчас есть смысл вводить только сахарок, который можно будет переделать на старые версии. Тут это логично, потому что есть до сих пор люди, которые сидят на ie7-9, которым про наши холивары "что лучше, Promise или await/async" вообще пофигу, у них не работает ни то, ни другое по дефалту.

У них async-await и так и так не работает, потому можно сделать нормальный интерфейс.

Я именно про этом, потому что там нужно, получается, 2 раза фигачить: 1 — async/await трансформировать в промисы; 2 — взять и подключить отдельную либу для того, чтобы хотя бы не нативыне, но промисы были.

Промисы — костыль своего времени из-за отсутствия евейта.

Нет, это не так. Невозможно сразу сделать оператор await, не вводя в язык промисы или их аналоги.

На счёт as они просто попытались спереть у питона… Но у них не получилось, потому что модули в js всё равно редатировать труднее, чем питоновские, там потому что имя модуля, из которого всё берётся находится раньше вещей, которые из него берутся, что в insert mode ещё хоть как-то можно понять. А вот когда тебе кроме того, что изменить импорты нужно ещё и путь к модулю запоминать, вот тут уже начинается портативный ад.

Лучше бы они раньше взяли бы пример с питона и сделали нормальную деструктуризацию. А импорты да — крайне кривые.

Глядя на некоторые из этих примеров использования промисов и правда может разболеться голова, ведь промисы как раз таки и были задуманы для решения callback-hell и этого адского уровня вложенности.


Но некоторые из них же можно переписать!
например, вместо


этого кода:
const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

я бы написал


попроще:
const makeRequest = () => {
  const requestPromise = getJSON().then((data) => {
    return data.needsAnotherRequest ? makeAnotherRequest(data) : data;
  });

  requestPromise.then(console.log);

  return requestPromise;
}

а вместо


вложенных промежуточных переменных
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return promise2(value1)
        .then(value2 => {
          // do something          
          return promise3(value1, value2)
        })
    })
}

сделал бы


плоский список:
const makeRequest = () => {
   const p1 = promise1();
   const p2 = p1.then((value1) => promise2(value1));
   const p3 = Promise.all([p1, p2]).then(([value1, value2]) => promise3(value1, value2));

   return p3;
}

но с другой стороны, не могу не согласиться, что с async/await это выглядит поопрятней =)

Можно-то можно, даже callback-hell можно переписать и превратить во что-то приятное и понятное

Но вот такое:
async function gogogo(queryList) {
    let resultJson = {}

    for (const query of queryList) {
        let json = {}
        const type = await getType(query)
        
        if (type === 1) {
            json = await getJson(query)
        }
        else if (type === 2) {
            const rawData = await getRawData(query)
            json = await rawToJson(rawData)
        }
        else {
            break
        }

        Object.assign(resultJson, json)
    }

    return resultJson
}

async function doSomething() {
    const resultJson = await gogogo(queryList)
    const html = templateGenerator(resultJson)
    console.log(hmtl)
}

doSomething()

Уже становится немного сложновато представить на промисах, особенно если какой-то еще логики надо добавить
Cорри, по понятным причинам не тестал. Попрошу обратить еще внимание на http://bluebirdjs.com/docs/api/promise.map.html#map-option-concurrency – что-то мне подсказывает что оно еще и работать будет быстрее так постарается вызвать все запросы пораньше(как работает async/await в случае цыкла – не уверен, но что-то мне кажется что наш код станет псеводо-синхроным для этого цыкла)

const Promise = require('bluebird')

function gogogo(queryList) {
    return Promise
      .resolve(queryList)
      .map(getType)
      // can be incapsulated in a function but original sample doesn't do that
      .map(function(type) { 
        if (type === 1) {
            return getJson(query)
        }
        else if (type === 2) {
            return getRawData(query)
              .rawToJson(rawData)
        }
        else {
          return {}
        }
      })
      .reduce(Object.assign, {})
}

function doSomething() {
    gogogo(queryList)
      .then(templateGenerator)
      .tap(console.log)
}

doSomething()
пока писал это понял – промисы нравятся тем кто предпочитает .map, .filter, .reduce циклам и наоборот async/await – любителям циклов

Хорошо, это или плохо – не знаю, это тема для извечного холивара. Но мне все же кажется, что async/await – позволяет писать в псевдо-синхронной манере игнорируя понимаю когда произойдет асинхронная операция

ну и заодно интересно – есть ли щанс достичь аналогичного поведения с async/await? с concurrency = Infinity или, например, concurrency = 10
Вот вам классическая задача.
Есть у вас массив урлов. Запрос по каждому из них возвращает ключ, и каждый, начиная со второго, принимает в запросе ключ, полученный от предыдущего запроса. Количество урлов не известно. Нужно получить последний ключ. И давайте договоримся, всевозможные хелперы из всяких bluebird и компании использовать нельзя.
Ради интереса, представим, что у нас еще очень маленький стэк, а урлов может быть больше 10к.
давайте договоримся что вы будете использовать – Pascal или js из 7 осла? Неиспользовать технологии – это луддизм

Кроме того, начните сначала с себя. Ваш комментарий без кода выглядит, мягко говоря, голословно
Неиспользовать технологии – это луддизм
Мы как раз обсуждаем технологию: async/await или промисы. Не надо тащить сюда ворох библиотек, облегчающих работу с промисами и инкапсулирующих бойлерплейт.

Вот вам пример.
declare const get: (url: string, key: string) => Promise<string>;
const getKey = async (initial: string, urls: string[]): Promise<string> => {
    let result = initial;
    for (const url of urls) {
        result = await get(url, result);
    }
    return result;
};


PS. простите мне мой TS
>Не надо тащить сюда ворох библиотек
одну и странно слышать это от человека предоставивщего пример на typescript

const Promise = require('bluebird')

const getKey = (initial, urls) =>
  Promise.resolve(urls)
  .reduce(
    key, url => get(url, key),
    initial
  )
Пожалуйста, вот вам обычный ES:
const getKey = async (initial, urls) => {
    let result = initial;
    for (const url of urls) {
        result = await get(url, result);
    }
    return result;
};


Что вы везде этот bluebird тащите? Можете руками написать асинхронный редьюс?
>Что вы везде этот bluebird тащите?
очевидно – нормальная обработка ошибок и приятные фичи

>Можете руками написать асинхронный редьюс?
думаю смогу, но давайте не будем это проверять. Потому что я попрошую взамен гарантий что вы сможете руками добавить в язык async/await
Кроме того, начните сначала с себя.
думаю смогу, но давайте не будем это проверять.

Вы что-то мечетесь, неужели так сложно без bluebird?

Потому что я попрошую взамен гарантий что вы сможете руками добавить в язык async/await
Они уже в языке и спеке.

reduce тоже есть в языке и в спеке.

Если вам позарез нужен асинхронный — держите:


function reduceAsync(array, fn, initial) {
    return array.reduce(
        (prev, current, index) => prev.then(prev2 => fn(prev2, current, index, array)),
        Promise.resolve(initial)
    );
}

Но я всегда простым обходился.

Так что по рукам или слились? вы берете рантайм от 51-го Firefox и допиливаете туда async/await

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

>неужели так сложно без bluebird?
ага, а еще без underscore, jquery и еще вагона библиотек, которые в той или иной мере вошли в язык, Browser API

На этом – давайте закончим дискуссию, а то она несколько в другую плоскость перерешла.
Так что по рукам или слились? вы берете рантайм от 51-го Firefox и допиливаете туда async/await
Это вы слились. Вы тащите стороннее апи для расширения существующей спецификации только для того, чтобы упорно пытаться решить задачу, для которой в язык введен отдельный инструмент, не используя этот инструмент. Главное преимущество async/await и генераторов — возможность удобно работать с циклами. Вы так здорово приводите примеры на промисах в ситуациях, когда с их помощью действительно можно решить поставленную задачу, а примера с циклом я от вас так и не дождался.

Ну что ж, действительно, закончим.
очевидно – нормальная обработка ошибок и приятные фичи

То есть, встроенные в язык Промисы по вашему — не юзабельны?

ну вот без await


const getKey = (prevKey, remainingUrls) => {
    if (!remainingUrls.length) {
        return Promise.resolve(prevKey);

    } else {
        return getKeyFromApi(remainingUrls[0]).then(newKey => {
            return getKey(newKey, remainingUrls.slice(1))
        });
    }
}
Я выше написал про маленький стэк. Можете без рекурсии?

насколько я помню, в случае асинхронных операций, стек не съедается таким образом (я даже проверил это для 18000 урлов с текущей глубиной стека в 17800)…
где-то даже видел "хак" в виде tail-call optimisation, когда себя же вызывают через setTimeout)


вот по памяти, я думаю, будет не очень оптимально, это да.

Чтобы было оптимально по памяти — надо вместо slice использовать растущий индекс. Получится даже оптимальнее чем через reduce.

там кроме слайсов создаётся ещё куча замыканий в виде анонимок, вызывающих эту функцию с новыми параметрами)

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

Я вам тут отвечу.

Спецификация промизов явным образом требует от рантайма чтобы колбек, переданный в then, запускался на пустом стеке (формально — не содержащем никаких пользовательских фреймов).
Про спеку и пустой стэк не знал, спасибо. Я не особо силен в этой внутрянке, но разве аргументы функции не образуют новый скоуп, который где-то должен лежать, даже при очистке стэка? Это уже к вопросу расхода памяти.
Так же нужно действительно хранить замыкания, как написали выше.

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


И таких цепочек хранится всего одна — для следующей итерации.

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

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

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

Вы так говорите как будто await ничего не выделяет при вызове :-)

Выделяет, но цикл — не рекурсия.

Опять двадцать пять… Почему вы считаете рекурсию безусловно хуже цикла, если она не требует больше памяти и не ест стек?

Ну где она не есть память-то?
Но это каждый раз будет новая цепочка,
Старая куда денется? Уничтожится? Зачем наматывать эти цепочки рекурсивно, когда можно просто пройтись в рамках одного скоупа циклом по массиву, сохраняя ссылку на ключ?
Сравните 2 примера выше — в рекурсивном варианте вы выделяете память каждый раз заново для всей начинки getKey при вызове. Слайс массива — это вообще надругательство.

Про слайс массива я уже тоже писал, его надо на переменную с индексом заменить или на использование итератора.


Прошу обратить внимание, что все существующие js-реализации async/await используют рекурсивный вызов на каждый оператор await. И нет оснований предполагать что рантайм браузера будет реализован сильно оптимальнее.

Await основан на генераторах, которые реализованы нативно в самом движке. Так что там с памятью все в порядке (надеюсь).
оно рекурсивное – вам сейчас расскажут про то что urlов больше чем размер стека. Вы можете это пофиксить извернувшись с .catch – но будет выглядеть уродски

Нет, эта функция — не рекурсивная и прекрасно работает на ограниченном стеке.


Спецификация промизов явным образом требует от рантайма чтобы колбек, переданный в then, запускался на пустом стеке (формально — не содержащем никаких пользовательских фреймов).

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

А вот это я с трудом представляю как изобразить, чтобы такой вариант заработал:
  return getRawData(query)
    .rawToJson(rawData)

При то, что и getRawData и rawToJson асинхронные функции. Либо тут надо быть гуру промисов, чтобы в этом разобраться

Но даже при этом всё равно получилось не то же самое, так как «return {}» не тоже самое что «break», и в случае с map перебор продолжится, а в моей случае предпологался выход из цикла
Либо тут надо быть гуру промисов, чтобы в этом разобраться
Нет, это просто опечатка, должно быть getRawData(query).then(rawToJson)
да, очепятка – такое бывает с кодом который нельзя протестировать

return getRawData(query)
              .then(rawToJson)


>Но даже при этом всё равно получилось не то же самое, так как «return {}» не тоже самое что «break», и в случае с map перебор продолжится, а в моей случае предпологался выход из цикла

как-то это не логично чуть-чуть(точно не continue)

но ок:

const Promise = require('bluebird')

function gogogo(queryList) {
    return Promise
      .resolve(queryList)
      .map(getType)
      .then((types) =>
        types.find(x => x !=== 1 && x !== 2)
      )
      .map(function(type) { // can be incapsulated in a function but original sample doesn't do that
        if (type === 1) {
            return getJson(query)
        }
        else {
            return getRawData(query)
              .rawToJson(rawData)
        }
      })
      .reduce(Object.assign, {})
}

function doSomething() {
    gogogo(queryList)
      .then(templateGenerator)
      .tap(console.log)
}

doSomething()


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

Будет как 10 лет назад: «Ищу php-разработчика со своим фреймворком – любителей писать с нуля, просим не беспокоить»
Давайте, если на то пошло откажемся от всех библиотек сразу тогда. Представьте как будет интересно – все руками.
И смешались в кучу кони люди. Не путайте часть спецификации языка и сторонние библиотеки.
Async/await не требует никаких библиотек для работы. Генераторы почти не требуют никаких библиотек для работы. Почти, потому-что можно раннер либо самому написать, либо взять из бабеля (в комментах выше), либо из co.

мы, вроде бы, обсуждаем фичу которая почти везде возможна только с транспилером.
Раз, два, три.
Мы же помним что это js? – тут все сначала стороняя библиотека, а потом идет в язык
мы, вроде бы, обсуждаем фичу которая почти везде возможна только с транспилером

Нее, мы обсуждаем фичу которая нативно работает в node, и во всех актуальных браузерах, кроме Edge и Opera Mini
http://caniuse.com/#feat=async-functions

как-то это не логично чуть-чуть(точно не continue)
но ок:

Ну вот, про это и речь, «Уже становится немного сложновато представить на промисах, особенно если какой-то еще логики надо добавить»

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

Дальше – рассуждайте сами, какой подход вам более покажется преемлемым

вопрос исключительно в том что стоит показывать равносильные примеры, а не те в которых один подход явно выгодней другого, но это не к вам вопрос – а к статье
Я про то и говорю:
Всё становится очень хрупким и нужно всю цепочку держать в голове, чтобы небольшие изменения внести. При этом никто не говорит, что это не возможно, возможно-то возможно
Достаточно чуть усложнить условие (приложение в процессе динамического развития, много экспериментов):

        const type = await getType(query)
        const subtype = await getSubType(query)
        if (type === 1 && subtype === 4) {
            json = await getJson(query)
        }
        else if (type === 2 && !subtype) {
            const rawData = await getRawData(query)
            json = await rawToJson(rawData)
        }
        else if (subtype === 1) {
            break
        }

И всё становится чуточку сложнее и нужно поломать голову, как это впихнуть в промисы без промисс-хелла.
И я уверен, что это получится сделать, но я не уверен, что можно это сделать за 10 секунд, как это делается на async-await

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

Вот зачем вы пишите Promise.resolve(queryList).map(getType) — когда можно написать queryList.map(getType)? И будет работать без сторонних библиотек.


Вы же сначала сами переусложняете код — а потом жалуетесь что вас не понимают.


PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!


function gogogo(queryList) {
    const items = queryList.map(query => {
        const type = await getType(query);

        if (type === 1) {
            return getJson(query)
        }
        else if (type === 2) {
            const rawData = await getRawData(query)
            return rawToJson(rawData)
        }
    })

    let resultJson = {}
    for (const item of await Promise.all(items)) {
        if (item === undefined) break;

        Object.assign(resultJson, item)
    }
    return resultJson;
}

И да, я знаю что этот код не полностью эквивалентен исходному.

Вот этот вариант мне нравится
Вот оно! Есть правда некоторые неточности с отсутствующими кое-где async и await, но посыл-то верный, каждый инструмент ввели в язык для своих целей и использовать нужно все.

Нет, await у меня все на месте! А вот про async я и правда позабыл напрочь...

getJson же асинхронный? Мм, ну по логике, из названия. Хотя по изначальному коду и не скажешь.

Edit: Ааа, items это же промисы :)
>Вот зачем вы пишите Promise.resolve(queryList).map(getType) — когда можно написать queryList.map(getType)?

Эти два куска кода совсем не эквиваленты. Посыл был показать concurrent и что выполнить n-запросов один за другим – это далеко не тоже самое что выполнить n-запросов «сразу». И что это важно – понимать эту разницу. Но это никто в упор не хочет видеть.

проблема вашего и моего кода в другом – мы делаем лишние вызовы к getType.

>PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!
Отличная мысль! давайте еще раз глянем в заглавие статьи. А потом посмотрим на примеры из нее – я не вижу там того что показал Shannon, но не смог сформулировать

А нужно было показать что в случаях с циклами с пост-условием(вот этот break) нам, таки, намного легче писать с async/await – и пофиг что это некрасиво, не-functional и вообще почти GOTO. И, наверное есть еще подобные примеры

В статье и комментариях я вижу как раз то о чем вы написали:

Promise.resolve([1, 2, 3]).map(getType)


сложнее чем(и впридачу, еквивалентно)

[1, 2, 3].map(query => {
        const type = await getType(query);
        return type
})


только в действительности первый вариант будет выполнятcя ~n времени(конечно, с оговорками), а второй 3*n времени. И если эту разницу игнорировать – то нам вообще не нужен async/await(как и promise), нам просто нужно синхронное IO

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

И дело не в функциональщине vs императивщине, а в том, что оба подхода лучше сочетать, а не впадать в крайности.
Все эти примеры выдуманы, чтобы показать упрощение синтаксиса, но вы зацепились за конкурентность

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

Вот упрощенный пример из рабочего проекта:
const [userdata, params] = await Promise.all([getUserData(), getParams()])
let bricks = []

if (params.allowedExtraData && userdata.needExtraData) {
    userdata.extraData = await getExtraData(userdata.id)
}

bricks.push(...params.bricksForModule[userdata.moduleType])
bricks.push(...params.bricksForType[params.mainType])

if (params.groups[userdata.groupType].isModerator) {
    bricks.push(...patams.templatesModerator)
}

const bricksData = await Promise.all(bricks)
...

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

Это ведь JavaScript, конечно может. Или не JavaScript?
ключевое слово здень – необъявленная. Тут можно выдумать каким образом это прокатило – но будет выглядеть натянуто, и скорее как оправдание.
Это ведь JavaScript, конечно может. Или не JavaScript?
Так и есть, мой промах, не учел что этот кусок кода в условии, которое может никогда не выполнится

Прикол в том, что даже если условие будет положительным вместо этой переменной подствиться undefined. А дальше уже соответствующие ошибки полезут, аля undefined is not a function, cannot read property <...> of undefined.

Да нее, будет «ReferenceError: patams is not defined» в любом случае, даже если без 'use strict' запускать

Единственное что, если без 'use strict' запускать, можно записать какое-нибудь значение в необъявленную переменную, она в глобальную область видимости попадет, но само по себе undefined не подставится
нет – со стороны выглядит будто вы этот кусок вырезали из кода
Как раз наоборот, код сократил раза в 3, а все переменные переименовал, чтобы легче смысл передать
Я не понял посыл вашего комментария.

с точкой зрения:
>PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!
я вполне согласен – ваш первый пример, если его вдумчиво «покурить», показал что есть ситуация в которой async/await упрощает жизнь. Но в статье об этом ни слова.

Про ваш второй пример – я такого сказать не могу. Ну заменили вы .then на await. Вроде как стало на один уровень вложености меньше, но стоит вам добавить обработку исключений и все вернестя на свои места.
то что вам не нужно пробрасывать через .then пары аргументов – это плюс.

Ну и он страно написан – await Promise.all(bricks). возможно вы где-то кусок кода удалили – но, даже так, это странно складывать промисы и что-то «статитеское» в один и тот же список. Скорее кто-то на всякий случай обернул чтобы не думать что там внутри. Есть еще вариант, что там в params промисы где-то лежат – но это тоже странно.

>По сути, обычно…
бывает по разному. давайте все же смотреть на картину в целом.

>А когда она требуется, то просто делается все через Promise.all
Если вас не затруднит, проилюстрируйте свою идею кодом — по, возможности, перепишите код из этого комментария
https://habrahabr.ru/company/ruvds/blog/326074/#comment_10166298 , само сабой – с учетом опечатки и без break так чтобы делало как можно больше запросов «паралельно»
И Остапа понесло…

Ну и он страно написан – await Promise.all(bricks)
bricks — это набор функции, которые возвращают промис.
Это модули, которые делают только свою маленькую работу, все они запускаются параллельно и их результат складывается в единый результат, а дальше в шаблонизаторе, уже в зависимости от наличия модуля будет активирован или нет нужный кусок шаблона

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

>А когда она требуется, то просто делается все через Promise.all
Если вас не затруднит, проилюстрируйте свою идею кодом — по, возможности, перепишите код из этого комментария
Вот этот пример: https://habrahabr.ru/company/ruvds/blog/326074/#comment_10167050

Но если вам нужен именно пример моего изначального примера:
Заголовок спойлера
async function gogogo(queryList) {
    let resultJson = {}
    let promiseList = []
    let rawDataPromiseList = []

    for (const query of queryList) {
        let json = {}
        const type = await getType(query)

        if (type === 1) {
            promiseList.push(getJson(query))
        }
        else if (type === 2) {
            rawDataPromiseList.push(getRawData(query))
        }
        else {
            break
        }
    }

    for (const rawData of await Promise.all(rawDataPromiseList)) {
        promiseList.push(rawToJson(rawData))
    }

    for (const json of await Promise.all(promiseList)) {
        Object.assign(resultJson, json)
    }

    return resultJson
}

И из цикла выйдем, и все запросы пойдут конкурентно, максимально насколько возможно в рамках данного примера. И даже больше, легко добавим более сложные условия:
        const type = await getType(query)
        const subtype = await getSubType(query)

        if (type === 1 && subtype === 4) {
            promiseList.push(getJson(query))
        }
        else if (type === 2 && !subtype) {
            rawDataPromiseList.push(getRawData(query))
        }
        else if (subtype === 1) {
            break
        }

>bricks — это набор функции, которые возвращают промис.
params.bricksForModule[userdata.moduleType] – тоесть это promise? или функция – если функция, то когда она вызывается?

За пример кода – спасибо. В принципе увидел, то что хотел.

Да, это массив промисов, через рест параметр они вливаются в общий массив промисов, которые потом вызываются параллельно
Типа комбинированный подход.

из плюсов:
не меняет порядок эллеметов( у вас promiseList «как бы упорядочен»)

const Promise = require('bluebird')

function gogogo(queryList) {
  const parts = []

  for (const query of queryList) {
    const type = await getType(query)
    if (type === 1) {
      parts.push(getJson(query))
    }
    else if (type === 2) {
      parts.push(
        getRawData(query).rawToJson(rawData)
      )
    } else {
      break
    }
  }
  return Promise
    .all(parts)
    .reduce(Object.assign, {})
}


В общем – спасибо за пример. Ну и всегда приятно пообщатся с человеком который подкрепляет свои суждения кодом. Приятных
только в действительности первый вариант будет выполнятcя ~n времени(конечно, с оговорками), а второй 3*n времени. И если эту разницу игнорировать – то нам вообще не нужен async/await(как и promise), нам просто нужно синхронное IO

Я ни коим образом не предлагаю эту разницу игнорировать! Напротив, я предлагаю совершенно другое: использовать async/await когда нужно последовательное выполнение и map + Promsie.all когда нужно параллельное.

Sign up to leave a comment.