Comments 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;
Так имхо читабельней.
Конкретно в данном случае он подходит. Но так бывает не всегда.
Сталкивался пару раз с ситуацией, когда надо в старом коде запустить параллельный процесс. Переводить на Promise.all
в таком случае означает изменить 30 строк, что выльется в приключения с git rebase перед пушем если кто-то еще правил этот метод и затруднит git blame в следующие пять лет поскольку сделает меня автором строк которые я не писал.
А альтернативный подход — это всего 2 измененные строки.
А я вот считаю, что лучше тронуть 30 строк и написать новое красивое решение (которым можно гордиться ближайшие 5 лет), чем костылить 2 и получать бяку в итоге :)
let user1 = hn.getUser({username: 'sama'});
let user2 = hn.getUser({username: 'pg'});
await user1; await user2;
console.log(user1, user2);
Почему try…catch
нивелирует красоту?
try..catch
увеличивает уровень вложенность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 намного короче и лучше читабельней.
Плюс, как я уже писал ниже, никто не запрещает комбинировать async с промисами, если это дает более читабельный код.
function withPromises() {
return action1().then(r1 => action2().then(r2 => r1 + r2));
}
function withPromises() {
return action1().then(r1 => {
return action2().then(r2 => r1 + r2)
});
}
ИМХО так лучше.
function withPromises() {
return action1()
.then(r1 => action2())
.then(r2 => r1 + r2);
}
Понятно, что если на каждом этапе надо делать больше, чем просто вызвать следующую функцию или сложить 2 значения, то надо будет разворачивать стрелочные функции в нормальные или выносить этот функционал в отдельные именованные функции.
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 тут ни при чем — на промизах все точно так же переклинило бы.
Довольно часто так делаю и до сих пор небыло никаких проблем.
Да, try-catch иногда выглядят не очень, но в целом стало удобней, чем с промисами.
Обернули бы этот await в try/catch и ошибка так же отлавливалась бы
обычный декларативный синтаксис
всё же, императивный.
"не дать программисту выполнять что-то долгое в основном потоке" — это общее свойство всех правильных подходов к построению UI, потому что альтернатива — фризы и подгаливания.
Не пишу на JS, просто зашел посмотреть
"Ничего в этом не понимаю, но мнение имею".
CSS анимации происходят в том же потоке, что и исполнение скриптов. Из-за этого исполнение скриптов приходится дробить на кванты по 15мс.
Многопоточность лучше использовать без разделяемого состояния.
Насчёт CSS я вас обманул. Был введён в заблуждение этой демкой, где оказывается анимация сделана через js, а не css: https://build-mbfootjxoo.now.sh/
Там уже побороли неработающий JIT под iOS? Добавили поддержку Win? Понаписали кроссплатформенных компонент, не требующих писать разный код для разных платформ?
По мне так лучше кордова с css анимациями в возможностью запуска в вебе или xamarin с полноценным компилируемым языком.
Главная причина «лагающего UI» обычно исключительно кривые руки.
Ну да, разумеется — чьи-то кривые руки это причина большинства проблем, и не только в UI. Но тем не менее, для UI вообще характерна однопоточность (не вообще приложения, а только один поток работает с UI), и если вы посмотрите — то множество широко известных фреймворков сделаны именно так. И на то есть серьезные причины.
А кривые руки — уже последствия той сложности, которая при этом возникает.
В других языках почему-то не обрезали программисту руки, а выполнение чего-то в отдельном потоке просто считается хорошим тоном.
Я работаю с UI уже не одно десятилетие, и знаю что где и как тормозит, зачем сразу унижать оппонента, если его не знаете?
П.С.: Спасибо за слив кармы.
mayorovp, Aquahawk, vassabi, justboris, parakhod.
Вы можете иметь свое мнение, но это будет мнение рядового обывателя. Это примерно как ругать работу врачей или других специалистов, в работе которых вы не разбираетесь.
Практической пользы от таких набросов — ноль.
П.С.: Спасибо за слив кармы.
Круто, вы — читер, знаете, кто вам карму сливает??
«Вы либо трусы наденьте, либо крестик снимите» (с)
В JS тоже никому ничего не обрезали, как и в любых других языках.
А вот среда выполнения (к которой относятся понятия «UI\core поток») node — да, задает ограничения. Так же как и андроид для джавы, и иОс для свифта. Попробуйте там выполнить что-то долгоиграюющее на UI event ( например при нажатии на кнопку) — тут же наступит «Сначала не дать программисту выполнять что-то долгое в основном потоке, а потом придумывать» (с)
Так что это не «хороший тон», а необходимость в любой среде и в любом языке, который вы под этой средой исполняете. Чем коллбеки и промисы хуже сишного PostMessage(WM_USER_DO_SOMETHING,...)? А ведь тоже — каноническийъ способ.
О чём, собственно, я пытаюсь порассуждать вслух. О том, что история в какой-то момент пошла не туда, и убогий язык стал мэйнстримом, который стали обвешивать костылями. А ведь чем проще решение, тем стабильнее оно работает.
Монада Promise — это общий подход, который сейчас применяется в языках C#, Python, Java (тут пока без синтаксиса async/await), хотят ввести в С++. Видел библиотеку и для Ruby, но не уверен насчет популярности.
В любом случае, из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом "программист должен страдать" и Haskell с его ленивыми вычислениями. Вы точно уверены что обещания — это костыли, а не новая парадигма асинхронного программирования?
В смысле — старая парадигма? Насколько я помню, этой парадигме лет 10 уже наверное минуло (не в виде async/await, а в изначальном).
из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом «программист должен страдать»Поподробнее пожалуйста.
Можете пояснить, как это можно было сделать "просто"? Ну, так чтобы не сломать все, что было до этого?
Вы считаете, что это просто? Во-первых, что там в браузере? А во-вторых, сломать таки можно многое. Ну хотя бы потому (на самый первый взгляд), что нужна будет какая-никакая синхронизация. По-моему они в браузере особо и не приживаются как раз по этой причине.
Да, с ним всё куда проще, чем с 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()
Получилось, конечно, красиво. Только вот у 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
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.
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 когда буфер забит — он будет неограниченно расширяться, что приведет к перерасходу памяти. Для небольших примеров это нормально, но в общем случае лучше бы приостановить генерацию контента когда буфер забился.
Другой вопрос, если денег на масштабирование нету, а нагрузка большая, то тут уже надо не выдумывать костыли на ноде, а на уровне операционной системы ограничивать кол-во соединений с вашим сервером, но тогда будут «лишние» недовольные пользователи, чьи запросы ваш сервер будет отклонять, просто потому, что физически их не сможет обслужить.
Конечно же не решает. Хакеру пофиг сколько серверов ложить атакой медленного чтения.
От такой атаки нужно защищаться по другому. Например установить таймаут на все соединения, например если в течении 3-5 секунд соединение все ещё висит, то обрубаем его. И если с этого айпишника приходит более N таких соединений за N время, то вообще блокируем его на N часов.
Нет, мое решение от этого как раз спасает, потому что ограничивает используемые соединением ресурсы. И как раз после этого все эти решения по ограничению числа соединений становятся эффективны.
Тем не менее, есть некоторая разница между сервером который может положить любой школьник и сервером который может положить не любой школьник.
JavaScript ES8 и переход на async / await