Как стать автором
Обновить

Комментарии 107

Оставлю альтернативный вариант распараллеливания для тех случаев когда не получается сделать красиво через Promise.all:


    const user1Promise = hn.getUser({username: 'sama'});

    const user2 = await hn.getUser({username: 'pg'});
    const user1 = await user1Promise;
Красивше будет
    const user1Promise = hn.getUser({username: 'sama'});
    const user2Promise = hn.getUser({username: 'pg'});
    
    const user1 = await user1Promise;
    const user2 = await user2Promise;

Так имхо читабельней.

Тогда уж и до такого недалеко


const [user1, user2] = await Promise.all([
  hn.getUser({username: 'sama'}),
  hn.getUser({username: 'pg'})
])

Кстати, mayorovp, а чем вам этот вариант не подошел?

Конкретно в данном случае он подходит. Но так бывает не всегда.


Сталкивался пару раз с ситуацией, когда надо в старом коде запустить параллельный процесс. Переводить на Promise.all в таком случае означает изменить 30 строк, что выльется в приключения с git rebase перед пушем если кто-то еще правил этот метод и затруднит git blame в следующие пять лет поскольку сделает меня автором строк которые я не писал.


А альтернативный подход — это всего 2 измененные строки.

А я вот считаю, что лучше тронуть 30 строк и написать новое красивое решение (которым можно гордиться ближайшие 5 лет), чем костылить 2 и получать бяку в итоге :)

А я вот не считаю использование Promise.all настолько сложным чтобы его использованием можно было гордиться 5 лет...


Для рефакторинга же придет время когда я останусь один на проекте :-)

При таком подходе печалит количество констант, которые требуются один раз, но для каждой из которых приходится придумывать имена :(

Так не заводите констант. Просто по месту использования приписывайте await перед переменной с промисом.

По-моему уж лучше через Promise.all, а то уж больно неочевидно получается, и ошибок так проще наделать.
Есть ещё вариант.
let user1 = hn.getUser({username: 'sama'});
let user2 = hn.getUser({username: 'pg'});
await user1; await user2;
console.log(user1, user2);
Нет, так работать не будет. То есть конкретно для `console.log` это сработает — но ведь в реальной программе эти запросы не для выдачи в лог делаются…
Что именно работать не будет?

То, что вы написали, работать не будет.


Оно выведет что-то типа Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }} Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }} вместо данных двух пользователей.

Странно, мне выводит данные двух пользователей.

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

Вы правы.

a = await a; b = await b;

можно типа такого. Хотя так уже мне самому не особо нравится.
НЛО прилетело и опубликовало эту надпись здесь

Почему try…catch нивелирует красоту?

try..catch увеличивает уровень вложенность

Вы же не пишете по два колбека на каждый .then? Так и здесь — try..catch только в конце — на самом верхнем уровне.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Зато код в стиле async/await удобнее отлаживать.

Обилия try…catch быть не должно в любом коде, это признак, что то не так, async лучше если нужно использовать результаты нескольких асинхронных операций вместе, например:
async function action1() {
  return 1;
}

async function action2() {
  return 2;
}

// promises
function withPromises() {
  let r1;
  return action1()
    .then(r => {
      r1 = r;
    })
    .then(action2)
    .then(r => r1 + r)
}

withPromises()
  .then(r => console.log('promise', r))
  .catch(e => console.error(e));

// async
async function withAsync() {
  return await action1() + await action2();
}

(async() => {
  try {
    console.log('async', await withAsync());
  } catch (e) {
    console.error(e)
  }
})()

async намного короче и лучше читабельней.
НЛО прилетело и опубликовало эту надпись здесь
Приведите не лукавый код с промисами, пусть даже синтетический (я не могу себе позволить копировать сюда рабочий код, да и вы наверно тоже), в котором оправданы множественные try…catch, а я попробую перевести это в async.
Плюс, как я уже писал ниже, никто не запрещает комбинировать async с промисами, если это дает более читабельный код.
НЛО прилетело и опубликовало эту надпись здесь

Вообще-то вы первый начали утверждать что try...catch нивелирует красоту, так что не переводите стрелки.

НЛО прилетело и опубликовало эту надпись здесь

Ну и зачем вы в таком случае написали комментарий, который в принципе недоказуем?

НЛО прилетело и опубликовало эту надпись здесь
function withPromises() {
  return action1().then(r1 => action2().then(r2 => r1 + r2));
}
Или так, но если в реальности кода будет больше, то придется разворачивать в
function withPromises() {
  return action1().then(r1 => {
    return action2().then(r2 => r1 + r2)
  });
}
так получиться Promise-hell
ИМХО так лучше.
function withPromises() {
  return action1()
  .then(r1 => action2())
  .then(r2 => r1 + r2);
}

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

у вас r1 в замыкании теряется. В последней строчке будет r1 is not defined.

согласен, но это не то, что я хотел показать.
В этом примере вообще я не вижу смысла делать 2 промиса последовательно, а если `r1` нужен для `action2()` то его надо туда явно передать.
Вы можете комбинировать, async функция всегда возвращает промис, даже если внутри ничего асинхронного не было.
async function getData() {
  let result = await asyncAction(10);

  console.log('result 1:', result);
  result += await asyncAction(20, true);

  console.log('unreachable code');
  result += await asyncAction(30);
  return result;
}

async function asyncAction(value, throwException = false) {
  if (throwException) {
    throw new Error(':(');
  }
  return value*2;
}

(async() => {
  console.log('begin getData');

  const data = await getData().catch(e => { // <===
    console.log('error occurred', e.message);
    return -1;
  })

  console.log('end getData', data);
})()


/* 
output: 
   begin getData
   result 1: 20
   error occurred :(
   end getData -1
*/
При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).

Вообще-то поддерживается с 7й версии если про ключ --harmony не забывать.

НЛО прилетело и опубликовало эту надпись здесь

Не вижу способа как асинхронный componentWillMount может хоть что-нибудь сломать. Вы можете привести какие-нибудь подробности?


Я поискал на гитхабе места где React вызывает componentWillMount — но я не нашел с ходу ни одного места где возвращаемое из componentWillMount хоть как-то использовалось бы.


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

НЛО прилетело и опубликовало эту надпись здесь
А можете скинуть реализацию метода? Прям интересно стало. Подозреваю, что оно просто грузило процессор какой-то неведомой фигней и async тут не при чем.

Довольно часто так делаю и до сих пор небыло никаких проблем.
Да, try-catch иногда выглядят не очень, но в целом стало удобней, чем с промисами.
НЛО прилетело и опубликовало эту надпись здесь

Обернули бы этот await в try/catch и ошибка так же отлавливалась бы

НЛО прилетело и опубликовало эту надпись здесь

Конкретно в вашем случае может и нет смысла. Я пытался донести, что проблема была вовсе не в async await, а в неправильной обработке исключений.

обычный декларативный синтаксис

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

"не дать программисту выполнять что-то долгое в основном потоке" — это общее свойство всех правильных подходов к построению UI, потому что альтернатива — фризы и подгаливания.

потому что если дать лочить основной поток подгрузкой картинок, то так и будут делать. И фризы будут как на криво написанных настольных приложениях
Справедливости ради, правило «не выполнять что-то долгое в основном потоке» применимо ко многим платформам. А уж в «UI-потоке\коллбеках» — так и вообще ко всем.
Не пишу на JS, просто зашел посмотреть

"Ничего в этом не понимаю, но мнение имею".

НЛО прилетело и опубликовало эту надпись здесь
С тем, кому вы отвечаете я не согласен, но и с вами тоже. Отсутвие многопоточности в javascript это большая проблема и одна из главных причин лагающего UI. Так как нельзя делать что либо не фризя UI.
НЛО прилетело и опубликовало эту надпись здесь

CSS анимации происходят в том же потоке, что и исполнение скриптов. Из-за этого исполнение скриптов приходится дробить на кванты по 15мс.


Многопоточность лучше использовать без разделяемого состояния.

НЛО прилетело и опубликовало эту надпись здесь

Насчёт CSS я вас обманул. Был введён в заблуждение этой демкой, где оказывается анимация сделана через js, а не css: https://build-mbfootjxoo.now.sh/

НЛО прилетело и опубликовало эту надпись здесь

Там уже побороли неработающий JIT под iOS? Добавили поддержку Win? Понаписали кроссплатформенных компонент, не требующих писать разный код для разных платформ?


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

НЛО прилетело и опубликовало эту надпись здесь

По винде допустим ОК, хотя поддерживается сторонней компанией.


А почему бы их не посравнивать? И это вы RN сейчас полноценной платформой назвали, который не более чем контейнер для JS?

НЛО прилетело и опубликовало эту надпись здесь

Я продам в 2 раза дешевле и сделаю в 2 раза быстрее сразу под 4 платформы.

НЛО прилетело и опубликовало эту надпись здесь

Что не так с веб-аппами?

Главная причина «лагающего UI» обычно исключительно кривые руки.

Ну да, разумеется — чьи-то кривые руки это причина большинства проблем, и не только в UI. Но тем не менее, для UI вообще характерна однопоточность (не вообще приложения, а только один поток работает с UI), и если вы посмотрите — то множество широко известных фреймворков сделаны именно так. И на то есть серьезные причины.


А кривые руки — уже последствия той сложности, которая при этом возникает.

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

П.С.: Спасибо за слив кармы.
mayorovp, Aquahawk, vassabi, justboris, parakhod.

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


Практической пользы от таких набросов — ноль.

НЛО прилетело и опубликовало эту надпись здесь

Про 30 лет — это вы пожалуй загнули :) javascript как языку всего примерно 21 год (в 1995 кажется он появился).

НЛО прилетело и опубликовало эту надпись здесь
Карму вам не сливал… до этого вашего комментария. Почему когда у людей кончаются аргументы — они начинают вопить о сливе кармы? Как будто карма — аргумент.
П.С.: Спасибо за слив кармы.

Круто, вы — читер, знаете, кто вам карму сливает??
>В других языках почему-то не обрезали программисту руки, а выполнение чего-то в отдельном потоке просто считается хорошим тоном.

«Вы либо трусы наденьте, либо крестик снимите» (с)
В JS тоже никому ничего не обрезали, как и в любых других языках.
А вот среда выполнения (к которой относятся понятия «UI\core поток») node — да, задает ограничения. Так же как и андроид для джавы, и иОс для свифта. Попробуйте там выполнить что-то долгоиграюющее на UI event ( например при нажатии на кнопку) — тут же наступит «Сначала не дать программисту выполнять что-то долгое в основном потоке, а потом придумывать» (с)

Так что это не «хороший тон», а необходимость в любой среде и в любом языке, который вы под этой средой исполняете. Чем коллбеки и промисы хуже сишного PostMessage(WM_USER_DO_SOMETHING,...)? А ведь тоже — каноническийъ способ.
Ох уж этот JavaScript… Сначала придумаем Promise, потом надстроим над ним сахар в виде async/await. В итоге за внешней простотой кода скрывается куча сложностей под капотом, что неизбежно приводит к побочным явлениям и неожиданным результатам.
О чём, собственно, я пытаюсь порассуждать вслух. О том, что история в какой-то момент пошла не туда, и убогий язык стал мэйнстримом, который стали обвешивать костылями. А ведь чем проще решение, тем стабильнее оно работает.

Монада Promise — это общий подход, который сейчас применяется в языках C#, Python, Java (тут пока без синтаксиса async/await), хотят ввести в С++. Видел библиотеку и для Ruby, но не уверен насчет популярности.


В любом случае, из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом "программист должен страдать" и Haskell с его ленивыми вычислениями. Вы точно уверены что обещания — это костыли, а не новая парадигма асинхронного программирования?

В смысле — старая парадигма? Насколько я помню, этой парадигме лет 10 уже наверное минуло (не в виде async/await, а в изначальном).

из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом «программист должен страдать»
Поподробнее пожалуйста.

Это была ирония. На самом деле это не девиз языка, а мое восприятие криков фанатиков go про дизайн их языка.

Promise придумали 40 лет назад — в JS его только реализовали.
Почему нельзя было, просто добавить блокировку IO в язык?!

Можете пояснить, как это можно было сделать "просто"? Ну, так чтобы не сломать все, что было до этого?

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

Да, с ним всё куда проще, чем с async/await.
В браузере никак ибо не стандарт.
Да нет, там всё в одном потоке исполняется, никакой особой синхронизации не нужно.

Ну, например, неработающий код из статьи:


let hn = require('@datafire/hacker_news').create();

(async () => {

  let storyIDs = await hn.getStories({storyType: 'top'});
  storyIDs.forEach(async itemID => {
    let details = await hn.getItem({itemID});
    console.log(details);
  });
  console.log('done!'); // Ошибка! Эта команда будет исполнена до того, как все вызовы getItem() будут завершены.

})();

C node-fibers можно переписать так:


const hn = require('@datafire/hacker_news').create();
const Future = retuire( 'fibers/future' )

Future.task(() => {

  let storyIDs = hn.getStories({storyType: 'top'}).wait();
  storyIDs.forEach( itemID => {
    let details = hn.getItem({itemID}).wait();
    console.log(details);
  });
  console.log('done!');

}).detach()

IO является не частью языка, а платформы. Для Javascript такой платформой является Node.js.
И блокирующие IO-операции там есть.

Попробовал я один AngularJS-проект с промисов на async/await переписать, еще всякие let, const, и стрелочные функции использовать. Babel настроил, как мог.
Получилось, конечно, красиво. Только вот у AngularJS свои собственные промисы, которые умеют делать $apply(). Можно, конечно windows.Promise переопределить, но у меня на проекте есть куча разных сторонних библиотек не связанных с ангуляром. Так что пришлось вызывать $scope.$apply() явно.
Стали очень плохо работать брякпоинты в Chrome Developer Console. Причем заметил я это, когда большая часть проекта уже была переписана. Мучал я babel, читал форумы, но как я понял, с отладкой es7 кода на браузере как-то все не очень радужно.

Хром давно умеет async/await нативно, в дев-сборке надо лишние плагины в babel по-отключать было.


А проблема своих промисов Ангуляра решается использованием генераторов обернутых в интерпретатор вместо асинхронных функций. Вот решение аналогичной проблемы в mobx-utils: https://github.com/mobxjs/mobx-utils/blob/master/src/async-action.ts

НЛО прилетело и опубликовало эту надпись здесь

Более чем практичная. Проект на 75kloc — сначала перевели тесты через бабель еще во времена 6-й ноды (~33kloc). Потом перевели всё остальное уже с 8-й нодой. Все довольны очень сильно, всё здорово.

Наглядный пример, где использовано и последовательное выполнение и параллельное. Копируйте весь код к себе, вставляйте, запускайте и проверяйте.
const http = require('http');

let promise = (payload, timeout = 10) => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(payload);
    }, timeout);
});


const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html; charset=UTF-8');
    (async () => {
        try {
            // Шаг 1: Ждем пока параллельно выполнится Promise.all (таймауты в 20ms)
            let results = await Promise.all([
                promise("[1]", 20),
                promise("[2]", 20)
            ]);
            // После того как дождались колбеков из Promise.all, запишем в сокет результат
            results.forEach(r => res.write(r));

            // Шаг 2: Ждем пока параллельно выполнится Promise.all (таймауты в 5ms)
            results = await Promise.all([
                promise("[3]", 5),
                promise("[4]", 5)
            ]);
            // После того как дождались колбеков из Promise.all, запишем в сокет результат
            results.forEach(r => res.write(r.toString()));

            // Шаг 3: Ждем пока поочередно выполнятся 2 промиса
            const a = await promise(1, 10);
            const b = await promise(3, 10);
            // Запишем промежуточный результат ввиде суммы переданных оргуметов
            res.write(`---Sum: ${(a + b)}---`);

            // Шаг 4: Ждем пока параллельно выполнится Promise.all (таймауты в 5ms)
            results = await Promise.all([
                promise("[5]", 5),
                promise("[6]", 5)
            ]);
            // После того как дождались колбеков из Promise.all, запишем в сокет результат
            results.forEach(r => res.write(r.toString()));

            // Ждем последовательного выполнения в цикле
            for (let i = 7; i <= 10; i++) {
                res.write(await promise(`[${i}]`, 100));
            }

            res.end(); // Закрываем соединение и сокет
        } catch (e) {
            console.log(e);
            res.end('Error');
        }
    })();
});

const port = 8081;
server.listen(port, '0.0.0.0', 65535, () => {
    console.log(`Server running at http://localhost:${port}/`);
});

Мне кажется, тут один уровень вложенности лишний: можно либо async выше перенести (http.createServer(async (req, res) => { ... }) либо try-catch ((async () => { ... })().catch(e => { ... }))


И я бы еще проверял результат вызова write и ждал события drain если вернулось false.

res.write(...):
Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory. 'drain' will be emitted when the buffer is free again.

Т.е. не важно, вернет она true или flase, в любом случае данные либо сразу ушли, либо встали в очередь в памяти на оправку и все равно уйдут, если конечно никаких внезапных крэшей не произойдет)

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

Проблема не актуальная, т.к. легко решается масштабированием.
Другой вопрос, если денег на масштабирование нету, а нагрузка большая, то тут уже надо не выдумывать костыли на ноде, а на уровне операционной системы ограничивать кол-во соединений с вашим сервером, но тогда будут «лишние» недовольные пользователи, чьи запросы ваш сервер будет отклонять, просто потому, что физически их не сможет обслужить.
Если бы проблема была неактуальной и решалась бы масштабированием — то события drain никто бы не вводил.
То есть вы хотите сказать, что масштабирование на 100% не решает эту проблему?

Конечно же не решает. Хакеру пофиг сколько серверов ложить атакой медленного чтения.

Так ваше решение не спасает от этого =) Так или иначе пользователи не будут получать ответ от сервера.
От такой атаки нужно защищаться по другому. Например установить таймаут на все соединения, например если в течении 3-5 секунд соединение все ещё висит, то обрубаем его. И если с этого айпишника приходит более N таких соединений за N время, то вообще блокируем его на N часов.

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

Я надеюсь вы понимаете, что в случае настоящей DDoS атаки, а не баловства ляжет 99.9% всех проектов. Поэтому не нужно параноить и создавать себе иллюзии, что ваш VDS с 1 гигом оперативы и один ядром процессора, становится неуязвимым против атак, если вы проверяете событие drain)

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

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