Использование промисов вместо коллбэков ведёт к написанию более лаконичного кода, который легче читать. Однако, тому, кто с ними не знаком, они могут показаться не особенно понятными. В этом материале я хочу показать базовые шаблоны работы с промисами и поделиться рассказом о проблемах, которые может вызвать их неумелое применение.
Обратите внимание на то, что здесь я буду использовать стрелочные функции. Если вы с ними не знакомы, стоит сказать, что устроены они несложно, но в этом случае советую прочесть материал об их особенностях.
Паттерны
В этом разделе я расскажу о промисах, и о том, как пользоваться ими правильно, продемонстрировав несколько шаблонов их применения.
?Использование промисов
Если вы применяете стороннюю библиотеку, которая уже поддерживает промисы, пользоваться ими довольно просто. А именно, нужно обратить внимание на две функции:
then()
и catch()
. Например у нас имеется API с тремя методами: getItem()
, updateItem()
, и deleteItem()
, каждый из которых возвращает промис:Promise.resolve()
.then(_ => {
return api.getItem(1)
})
.then(item => {
item.amount++
return api.updateItem(1, item);
})
.then(update => {
return api.deleteItem(1);
})
.catch(e => {
console.log('error while working on item 1');
})
Каждый вызов
then()
создаёт очередной шаг в цепочке промисов. Если в любом месте цепочки происходит ошибка, вызывается блок catch()
, который расположен за сбойным участком. Методы then()
и catch()
могут либо вернуть некое значение, либо новый промис, и результат будет передан следующему оператору then()
в цепочке.Вот, для сравнения, реализация той же логики с помощью коллбэков:
api.getItem(1, (err, data) => {
if (err) throw err;
item.amount++;
api.updateItem(1, item, (err, update) => {
if (err) throw err;
api.deleteItem(1, (err) => {
if (err) throw err;
})
})
})
Первое отличие этого фрагмента кода от предыдущего заключается в том, что в случае с коллбэками мы должны включать обработку ошибок на каждом шаге процесса, вместо использования единственного блока для обработки всех ошибок. Вторая проблема с коллбэками больше относится к стилю. Блок кода, представляющий каждый из шагов, выровнен по горизонтали, что мешает воспринимать последовательность выполнения операций, очевидную при взгляде на код, основанный на промисах.
?Преобразование коллбэков в промисы
Один из первых приёмов, который полезно изучить при переходе с коллбэков на промисы, заключается в преобразовании коллбэков в промисы. Потребность в подобном может возникнуть в том случае, если вы, например, работаете с библиотекой, которая всё ещё использует коллбэки, или с собственным кодом, написанном с их применением. Перейти от коллбэков к промисам не так уж и сложно. Вот пример преобразования функции Node
fs.readFile
, основанной на коллбэках, в функцию, которая задействует промисы:function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
})
})
}
readFilePromise('index.html')
.then(data => console.log(data))
.catch(e => console.log(e))
Краеугольный камень этой функции — конструктор
Promise
. Он принимает функцию, которая, в свою очередь, имеет два параметра — resolve
и reject
, тоже являющиеся функциями. Внутри этой функции и выполняется вся работа, а когда мы её завершаем, мы вызываем resolve
в случае успеха, и reject
в том случае, если произошла ошибка.Обратите внимание на то, что в результате должно быть вызвано что-то одно — либо
resolve
, либо reject
, и этот вызов должен быть выполнен лишь один раз. В нашем примере, если fs.readFile
возвращает ошибку, мы передаём эту ошибку в reject
. В противном случае мы передаём данные файла в resolve
.?Преобразование значений в промисы
В ES6 есть пара удобных вспомогательных функций для создания промисов из обычных значений. Это
Promise.resolve()
и Promise.reject()
. Например, у вас может быть функция, которой нужно возвратить промис, но которая обрабатывает некоторые случаи синхронно:function readFilePromise(filename) {
if (!filename) {
return Promise.reject(new Error("Filename not specified"));
}
if (filename === 'index.html') {
return Promise.resolve('<h1>Hello!</h1>');
}
return new Promise((resolve, reject) => {/*...*/})
}
Обратите внимание на то, что вы можете передать что угодно (или ничего) при вызове
Promise.reject()
, однако, рекомендуется всегда передавать этому методу объект Error
.?Одновременное выполнение промисов
Promise.all() —
это удобный метод для одновременного выполнения массива промисов. Например, скажем, у нас есть список файлов, которые мы хотим прочитать с диска. С использованием созданной ранее функции readFilePromise
, решение этой задачи может выглядеть так:let filenames = ['index.html', 'blog.html', 'terms.html'];
Promise.all(filenames.map(readFilePromise))
.then(files => {
console.log('index:', files[0]);
console.log('blog:', files[1]);
console.log('terms:', files[2]);
})
Я даже пытаться не буду писать эквивалентный код с использованием традиционных коллбэков. Достаточно сказать, что такой код будет запутанным и подверженным ошибкам.
?Последовательное выполнение промисов
Иногда одновременное выполнение нескольких промисов может приводить к неприятностям. Например, если вы попробуете получить множество ресурсов из API с использованием
Promise.all
, это API, через некоторое время, когда вы превысите ограничение на частоту обращений к нему, вполне может начать выдавать ошибку 429.Одно из решений этой проблемы заключается в том, чтобы запускать промисы последовательно, один за другим. К сожалению, в ES6 нет простого аналога
Promise.al
l для выполнения подобной операции (хотелось бы знать — почему?), но тут нам может помочь метод Array.reduce:let itemIDs = [1, 2, 3, 4, 5];
itemIDs.reduce((promise, itemID) => {
return promise.then(_ => api.deleteItem(itemID));
}, Promise.resolve());
В данном случае мы хотим ждать завершения текущего обращения к
api.deleteItem()
прежде чем выполнять следующий вызов. Этот код демонстрирует удобный способ оформления операции, которую иначе пришлось бы переписывать, используя then()
для каждого идентификатора элемента:Promise.resolve()
.then(_ => api.deleteItem(1))
.then(_ => api.deleteItem(2))
.then(_ => api.deleteItem(3))
.then(_ => api.deleteItem(4))
.then(_ => api.deleteItem(5));
?Гонка промисов
Ещё одна удобная вспомогательная функция, которая имеется в ES6 (хотя я и не особенно часто ей пользуюсь), это —
Promise.race
. Так же, как и Promise.all
, она принимает массив промисов и выполняет их одновременно, однако, возврат из неё осуществляется как только любой из промисов будет выполнен или отклонён. Результаты других промисов при этом отбрасываются.Например, создадим промис, который завершается с ошибкой по прошествии некоторого времени, задавая ограничение на выполнение операции по чтению файла, представленной другим промисом:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(reject, ms);
})
}
Promise.race([readFilePromise('index.html'), timeout(1000)])
.then(data => console.log(data))
.catch(e => console.log("Timed out after 1 second"))
Обратите внимание на то, что другие промисы продолжат выполняться — вы просто не увидите их результатов.
?Перехват ошибок
Обычный способ перехвата ошибок в промисах заключается в добавлении в конец цепочки блока
.catch()
, который будет перехватывать ошибки, возникающие в любом из предшествующих блоков .then()
:Promise.resolve()
.then(_ => api.getItem(1))
.then(item => {
item.amount++;
return api.updateItem(1, item);
})
.catch(e => {
console.log('failed to get or update item');
})
Здесь вызывается блок
catch()
, если либо getItem
, либо updateItem
завершится с ошибкой. Но что, если совместная обработка ошибок нам не нужна и требуется обрабатывать ошибки, происходящие в getItem
, раздельно? Для этого достаточно вставить ещё один блок catch()
сразу после блока с вызовом getItem —
он даже может вернуть другой промис:Promise.resolve()
.then(_ => api.getItem(1))
.catch(e => api.createItem(1, {amount: 0}))
.then(item => {
item.amount++;
return api.updateItem(1, item);
})
.catch(e => {
console.log('failed to update item');
})
Теперь, если
getItem()
даст сбой, мы вмешиваемся и создаём новый элемент.?Выбрасывание ошибок
Код внутри выражения
then()
стоит воспринимать так, будто он находится внутри блока try
. И вызов return Promise.reject()
, и вызов throw new Error()
приведут к выполнению следующего блока catch()
.Это означает, что ошибки времени выполнения также вызывают срабатывание блоков
catch()
, поэтому, когда дело доходит до обработки ошибок, не стоит делать предположений об их источнике. Например, в следующем фрагменте кода мы можем ожидать, что блок catch()
будет вызван только для обработки ошибок, появившихся при работе getItem
, но, как показывает пример, он реагирует и на ошибки времени выполнения, возникшие внутри выражения then()
:api.getItem(1)
.then(item => {
delete item.owner;
console.log(item.owner.name);
})
.catch(e => {
console.log(e); // Cannot read property 'name' of undefined
})
?Динамические цепочки промисов
Иногда нужно сконструировать цепочку промисов динамически, то есть — добавляя дополнительные шаги при выполнении каких-то условий. В следующем примере, прежде чем прочесть заданный файл, мы, при необходимости, создаём файл блокировки:
function readFileAndMaybeLock(filename, createLockFile) {
let promise = Promise.resolve();
if (createLockFile) {
promise = promise.then(_ => writeFilePromise(filename + '.lock', ''))
}
return promise.then(_ => readFilePromise(filename));
}
В подобной ситуации нужно обновить значение
promise
, использовав конструкцию вида promise = promise.then(/*...*/)
. С этим примером связано то, что мы рассмотрим ниже в разделе «Множественный вызов .then()».Анти-паттерны
Промисы — это аккуратная абстракция, но работа с ними полна подводных камней. Тут мы рассмотрим некоторые типичные проблемы, с которыми мне доводилось сталкиваться, работая с промисами.
?Реконструкция ада коллбэков
Когда я только начал переходить с коллбэков на промисы, я обнаружил, что от некоторых старых привычек отказаться тяжело, и поймал себя на том, что вкладываю друг в друга промисы так же, как коллбэки:
api.getItem(1)
.then(item => {
item.amount++;
api.updateItem(1, item)
.then(update => {
api.deleteItem(1)
.then(deletion => {
console.log('done!');
})
})
})
На практике такие конструкции не требуются практически никогда. Иногда один или два уровня вложенности могут помочь сгруппировать связанные задачи, но вложенные промисы практически всегда можно переписать в виде вертикальной цепочки, состоящей из
.then()
.?Отсутствие команды возврата
Часто встречающаяся и вредная ошибка, с которой я сталкивался, заключается в том, что в цепочке промисов забывают о вызове
return
. Например, можете найти ошибку в этом коде?api.getItem(1)
.then(item => {
item.amount++;
api.updateItem(1, item);
})
.then(update => {
return api.deleteItem(1);
})
.then(deletion => {
console.log('done!');
})
Ошибка заключается в том, что мы не поместили вызов
return
перед api.updateItem
в строке 4, и этот конкретный блок then()
разрешается немедленно. В результате api.deleteItem()
, вероятно, будет вызвано до завершения вызова api.updateItem()
.По моему мнению, это — основная проблема с промисами ES6, и она часто ведёт к их непредсказуемому поведению. Проблема заключается в том, что
then()
может вернуть либо значение, либо новый объект Promise
, при этом он вполне может вернуть и undefined
. Лично я, если бы отвечал за API промисов JavaScript, предусмотрел бы выдачу ошибки времени выполнения, если бы блок .then()
возвращал undefined
. Однако, подобное в языке не реализовано, поэтому сейчас нам лишь остаётся быть внимательными и выполнять явный возврат из любого создаваемого нами промиса.?Множественный вызов .then()
В соответствии с документацией, вполне можно вызывать
.then()
много раз в одном и том же промисе, при этом коллбэки будут вызваны в том же порядке, в котором они зарегистрированы. Однако, я никогда не видел реальной причины для того, чтобы так поступать. Подобные действия могут вести к непонятным эффектам при использовании возвращаемых промисами значений и при обработке ошибок:let p = Promise.resolve('a');
p.then(_ => 'b');
p.then(result => {
console.log(result) // 'a'
})
let q = Promise.resolve('a');
q = q.then(_ => 'b');
q = q.then(result => {
console.log(result) // 'b'
})
В этом примере, так как мы не обновляем значение
p
при следующем вызове then()
, мы никогда не увидим возврата 'b'
. Промис q
более предсказуем, его мы обновляем каждый раз, вызывая then()
.То же самое применимо и к обработке ошибок:
let p = Promise.resolve();
p.then(_ => {throw new Error("whoops!")})
p.then(_ => {
console.log('hello!'); // 'hello!'
})
let q = Promise.resolve();
q = q.then(_ => {throw new Error("whoops!")})
q = q.then(_ => {
console.log('hello'); // Сюда мы никогда не попадём
})
Тут мы ожидаем выдачу ошибки, которая прервёт выполнение цепочки промисов, но так как значение
p
не обновляется, мы попадаем во второй then()
.Множественный вызов
.then()
позволяет создать из исходного промиса несколько новых независимых промисов, однако, мне до сих пор не удалось найти реального применения для этого эффекта.?Смешивание коллбэков и промисов
Если вы используете библиотеку, основанную на промисах, но работаете над проектом, основанном на коллбэках, легко попасться в ещё одну ловушку. Избегайте вызовов коллбэков из блоков
then()
или catch() —
в противном случае промис поглотит все следующие ошибки, обработав их как часть цепочки промисов. Вот пример оборачивания промиса в коллбэк, который, на первый взгляд, может показаться вполне подходящим для практического использования:function getThing(callback) {
api.getItem(1)
.then(item => callback(null, item))
.catch(e => callback(e));
}
getThing(function(err, thing) {
if (err) throw err;
console.log(thing);
})
Проблема здесь заключается в том, что в случае ошибки мы получим предупреждение «Unhandled promise rejection», несмотря на то, что блок
catch()
в цепочке присутствует. Это так из-за того, что callback()
вызывается и внутри then()
, и внутри catch()
, что делает его частью цепочки промисов.Если вам абсолютно необходимо обернуть промис в коллбэк, вы можете использовать функцию
setTimeout
, или process.nextTick
в Node.js для того, чтобы выйти из промиса:function getThing(callback) {
api.getItem(1)
.then(item => setTimeout(_ => callback(null, item)))
.catch(e => setTimeout(_ => callback(e)));
}
getThing(function(err, thing) {
if (err) throw err;
console.log(thing);
})
?Неперехваченные ошибки
Обработка ошибок в JavaScript — странная штука. Она поддерживает классическую парадигму
try/catch
, но не поддерживает средства обработки ошибок в вызванном коде вызывающей его конструкцией, как это сделано, например, в Java. Однако, в JS распространено использование коллбэков, первым параметром которых является объект ошибки (такой коллбэк называют ещё «errback»). Это вынуждает конструкцию, вызывающую метод, как минимум, учитывать возможность ошибки. Вот пример с библиотекой fs
:fs.readFile('index.html', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
})
Работая с промисами, легко забыть о том, что ошибки надо явным образом обрабатывать. Особенно это актуально в тех случаях, когда речь идёт об операциях, восприимчивым к ошибкам, таким, как команды для работы с файловой системой или для доступа к базам данных. В текущих условиях, если не перехватить отклонённый промис, в Node.js можно увидеть довольно-таки неприглядное предупреждение:
(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops!
(node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Для того, чтобы этого избежать, не забывайте добавлять
catch()
в конец цепочек промисов.Итоги
Мы рассмотрели некоторые паттерны и анти-паттерны использования промисов. Надеюсь, вы нашли здесь что-нибудь полезное. Однако, тема промисов весьма обширна, поэтому вот — несколько ссылок на дополнительные ресурсы:
- Материалы по промисам от Mozilla
- Введение в промисы от Google
- Обзор промисов, подготовленный Дэйвом Атчли
- Вот и вот — дополнительные материалы по паттернам и анти-паттернам
Уважаемые читатели! Как вы используете промисы в своих Node.js-проектах?
Комментарии (51)
Aingis
05.10.2017 15:37-3Про util.promisify меня уже опередили, но, похоже, вы не до конца разобрались в теме промисов.
Только что проверил в консоли:let p = Promise.resolve('a'); p.then(_ => 'b'); p.then(result => { console.log(result) // 'a' })
Promise.resolve('a') .then(_ => 'b') .then(result => { console.log(result); // 'b' });
Всё дело в том, когда резолвится промис. Если сделатьPromise.resolve()
от не thennable значения, то он будет разрезолвен сразу.
Если вам абсолютно необходимо обернуть промис в коллбэк, вы можете использовать функцию
Не проще ли сразу воспользоваться вторым атрибутомsetTimeout
, илиprocess.nextTick
в Node.js для того, чтобы выйти из промиса:
function getThing(callback) { api.getItem(1) .then(item => setTimeout(_ => callback(null, item))) .catch(e => setTimeout(_ => callback(e))); }
then()
? Тогда проблемы двойного вызова не будет.
function getThing(callback) { api.getItem(1).then( item => callback(null, item), e => callback(e) ); }
justboris
05.10.2017 16:01+6Первые два фрагмента кода отличаются
p.then(...); p.then(...);
это не то же самое что
p.then().then()
Поэтому в первом случае
a
, а во второмb
, никаких подвохов.Aingis
06.10.2017 14:16-2Вы отлично справились с заданием «найди 10 отличий». Для этого цитата и была приведена. Однако, суть от вас ускользнула. В случае
колбэки выполняются последовательно. Но с чего автор взял, что в следующий колбэк не из цепочки придёт результат предыдущего? Это ему никто не обещал, и это ниоткуда не следует. Тот случай, когда сам придумал — сам опроверг.p.then(...); p.then(...);
P.S. Путаницы ещё и добавляет фраза в тексте
По моему мнению, это — основная проблема с промисами ES6, и она часто ведёт к их непредсказуемому поведению. Проблема заключается в том, что
Это не так!then()
может вернуть либо значение, либо новый объект Promise, при этом он вполне может вернуть и undefined.then()
всегда возвращает Promise. А вот колбэк может вернуть как Promise, так и любое значение. В результате, даже когда знаешь правильный ответ, сложно понять что имелось в виду на самом деле.
На мой взгляд, лучше ничего не писать, чем писать такой путанный текст, который с большой вероятностью может быть прочитан неправильно. Автор вроде бы разобрался, но не может аккуратно сформулировать механизм работы — отсюда, видимо, и «антипаттерны», которые всего-навсего личние глюки от неполного непонимания.
justboris
05.10.2017 16:04Не проще ли сразу воспользоваться вторым атрибутом then()?
При подходе
promise.then(() => doSomething()).catch(...)
блок catch перехватит не только ошибки в оригинальном промисе, но и если что-то пойдет не так вdoSomething
. Это бывает полезно.
saaivs
05.10.2017 15:55+3Пробежал по диагонали. Первое, что пришло в голову — это поском по страницы поискать «async/await». На момент написания комментария нашел единственное упоминание в разделе ПОХОЖИЕ ПУБЛИКАЦИИ под названием «Async/await: 6 причин забыть о промисах»
Собственно, что и хотел сказать :)
PS: Понимаю, что перевод и «мопед не мой», но все-таки…wert_lex
05.10.2017 20:25+2Ну, для того, чтобы полноценно пользоваться async/await понимать как работают промисы всё же необходимо.
vtvz_ru
05.10.2017 18:17+2Спасибо за указанные тонкости и приемы работы с промисами. Теперь мой код станет чуточку лучше)
Предпочитаю использовать async/await. Но насколько я знаю, async/await всего лишь синтаксисический сахар над промисами, и ничто не мешает мне использовать Promise.all с функциями, которые async. Или я ошибаюсь?
maolo
06.10.2017 08:26let filenames = ['index.html', 'blog.html', 'terms.html']; Promise.all(filenames.map(readFilePromise)) .then(files => { console.log('index:', files[0]); console.log('blog:', files[1]); console.log('terms:', files[2]); })
По-моему, использовать map в данном случае не лучшее решение — этот ведь синхронный перебор, и обработка файлов будет поочередной, а не параллельной. Или я не прав?
Senyaak
06.10.2017 08:26В принципе пролистав статью ничего нового не увидел, но новичкам будет очень полезно. Автору совутую добавить сюда в паттерны yield'ы async'и и generator's)
jehy
06.10.2017 09:57Забыли про генераторы и про то, что даже с нативной поддержкой промисы, bluebird быстрее и обладает чудесным богатым api. Пользуюсь им в разработке на восьмой ноде.
mayorovp
06.10.2017 12:22Из bluebird выкинуты многие полезные доработки, которые вошли в нативные.
Например, bluebird не сохраняет стек вызовов — удачи в ловле ошибок. Еще bluebird вызывает при необходимости продолжения синхронно — можно самому себе устроить состояние гонки даже в однопоточном языке.
Так что начинающим (а пост-то явно для них написан) я бы bluebird не рекомендовал.
jehy
06.10.2017 12:30delay, cancel, spread, promisify, timeout, inspection — там есть огромное количество фич, без которых, конечно, жить можно, но гораздо печальнее. В нативных, кажется, даже finally нет.
justboris
06.10.2017 12:52+1- Про promisify написано в первом же комменте этого треда
delay = promisify(setTimeout)
- spread — деструктуризация аргументов дает то же самое
Promise.all(...).then(([a,b,c]) => console.log(a,b,c))
- cancel — успешно решается на уровне библиотек. Например в axios это делается через токен.
- finally есть в обычном try/catch, который можно использовать в асинхронных функциях.
Если bluebird позволяет стрелять в ногу как говорит mayorovp, то я однозначно за нативные промисы, если нет очень крайней необходимости в скорости или других фишках bluebird.
mayorovp
06.10.2017 13:12+1Нее,
delay = promisify(setTimeout)
работать не будет, порядок аргументов не тот. Тут надо вручную:
const delay = time => new Promise(resolve => setTimeout(resolve, time));
justboris
06.10.2017 15:15А вы попробуйте.
Нодовский promisify позволяет подменять возвращаемый результат через специальный символ. И для setTimeout этот кастомный символ уже определен
jehy
06.10.2017 13:46Про promisify написано в первом же комменте этого треда
Да я видел, но мне не нравится мне использовать его из util. Из Promise гораздо логичнее и чище.
cancel — успешно решается на уровне библиотек
Ага, например, на уровне request-promise это решается через cancel от bluebird. Зачем плодить лишние сущности?
finally есть в обычном try/catch, который можно использовать в асинхронных функциях.
Ээээ. Так мне нужно его в цепочке использовать, finally от обычного try catch вот ни разу не поможет. Или вы имели в виду — использовать его в связке с async/await?
C delay уже сказали, для timeout тоже надо будет какой-то костыль делать… Кстати, со стэком у меня как-то никогда не было проблем, а про race condition фразу я, честно говоря, не понял.
mayorovp
06.10.2017 13:53q.then(() => this.loading = false); this.loading = true;
Если продолжение будет выполнено синхронно — получится упс.
jehy
06.10.2017 14:18Как-то не возникало мысли так писать, гораздо логичнее
q .then(() => this.loading = true) .then(() => doSmth()) .then(() => this.loading = false)
wheercool
06.10.2017 12:35Не в защиту bluebird, но состояние гонки можно легко получить и в асинхронной модели.
Многие забывают, что ответы могут приходить не в том же порядке, что и запросы
freeart
06.10.2017 12:53+1Вообще-то в catch войдет только синхронный вызов throw
api.getItem(1) .then(item => { delete item.owner; //async throw setTimeout(()=>{item.owner.name},0) }) .catch(e => { console.log("tt",e); // Cannot read property 'name' of undefined })
встречал это непонимание концепции в различных библиотеках при ловле ошибок и ожидание попадания обработчика в catch
Aingis
06.10.2017 13:09Достаточно вернуть промис.
freeart
06.10.2017 13:17api.getItem(1) .then(item => { delete item.owner; //async throw return new Promise((resolve)=>{ setTimeout(()=>{item.owner.name; resolve()},0) }) }) .catch(e => { console.log("tt",e); // Cannot read property 'name' of undefined })
Так? setTimeout это пример реализации сторонней, либо своей библиотеки в которой произошла ошибка при асинхронном вызове. Т.е. метод библиотеки вернул промис, но внутри он упал на ошибке.
mayorovp
06.10.2017 13:43-1Конечно же если чужая библиотека намеренно или случайно прячет от вас ошибку — ее достать не получится.
Но это просто означает что библиотека кривая.
freeart
06.10.2017 13:48а вы видели где-то библиотеку или продукт лишенный ошибок? Мир не совершенен. И люди, которые ждут от промис катч слишком многого.
lega
06.10.2017 14:33Незнаю насколько это антипаттерн, но кое где использую такой велосипед (foo, bar возвращают новые промисы):
function process(list) { let promises = []; promises.push(foo()); promises.push(bar()); list.forEach(() => promises.push(bar())); return Promise.all(promises); }
Вместо того чтобы вручную собирать все новые промисы, вызываю Promise.wait которая это делает за меня, код выше превращается в:
function process(list) { return Promise.wait(() => { foo(); bar(); list.forEach(bar); }); }
hopefaq
06.10.2017 15:02+2Ваш код с Promise.all в ES6 можно переписать таким образом:
function process(list) { return Promise.all([ foo(), bar(), ...list.map(bar) ]); }
И что за метод такой Promise.wait? В документации про него ни слова нету.lega
06.10.2017 17:02Не, ну это же упрощенный пример, перепешите тогда такой вариант (preload возвращает промис):
Суть в том что есть некий синхронный код с глубоким стеком и в определенный момент где-то на глубоком уровне появилась асинхронная операция, и вам сверху нужно дождаться её завершения. Конечно можно начать конвертировать/рефакторить весь проект, превращать синхронные в ассинхронные/проброс промиса наверх, но выглядит это не очень. Простой отлов новых промисов выглядит куда приятнее (и по большей части работает как надо).class User { constructor(raw) { Object.assign(this, raw); this.links.forEach(preload); this.children = this.children.map((raw) => new User(raw)); } } // sync variant var users = rawUsers.map((raw) => new User(raw)); // async variant Promise.wait(() => { return rawUsers.map((raw) => new User(raw)); }).then((users) => {});
И что за метод такой Promise.wait?
Я написал, что это велосипед.justboris
06.10.2017 18:08+1Конечно можно начать конвертировать/рефакторить весь проект, превращать синхронные в ассинхронные/проброс промиса наверх
Нужно начать рефакторить весь проект. В противном случае отхватите багов, потому что кто-то забудет поставить
Promise.wait
. Или наоборот, поставитPromise.wait
внутри другогоPromise.wait
.
Ну и покажите реализацию в коде, интересно посмотреть, какими хаками вы это сделали.
lega
06.10.2017 21:23Нужно начать рефакторить весь проект.
И можете не вписаться в дедлайны…
потому что кто-то забудет поставить Promise.wait
А если кто-то забудет поставить Promise.all? Не пройдет тестирование и пофиксится. А вот если наченете рефакторить (а если там 500к кода?) то наделать ошибок шансов больше.
Или наоборот, поставит Promise.wait внутри другого Promise.wait.
Работает как и ожидается.
какими хаками вы это сделали.
Ничего сверх-естественного, например можно так в 15 строк (на проде вариант получше использую): jsfiddle.net/lega911/pvovavLe
PS: может вы зарефакторите мой пример выше? интересно посмотреть как сильно распухнет код.vintage
06.10.2017 22:11Зачем так извращаться, если есть node-fibers, который делает ровно то, что вам надо?
justboris
06.10.2017 23:45+1Сайд-эффекты в конструкторе это уже плохо. У меня бы получилось как-то так
class User { constructor(raw) { Object.assign(this, raw); this.children = this.children.map(c => new User(c)); } load() { return fetch(...).then(() => Promise.all( this.children.map(c => c.load()) )) } } const users = rawUsers.map(user => new User(user)); Promise.all(users.map(user => user.load())).then(() => { // что-то делаем с готовыми users })
ну и совет от кэпа: если сразу делать нормально (имеется в виду всегда возвращать промис из асинхронных операций), то потом не придется переделывать, чтобы успеть в дедлайны.
justboris
06.10.2017 23:47Ну и по поводу вашей имплементации: а с нативными API, типа
fetch
оно как работает?
Как я понимаю, они ваш переопредленный класс Promise не увидят, будут пользоваться стандартным.
lega
07.10.2017 01:57Сайд-эффекты в конструкторе
Смотря что считать сайд-эффектами, вообщем это не аргумент.
У меня бы получилось как-то так
Код сложнее (имхо), кода больше, лишняя функция («логика/апи»), итого большой реальный код может не слабо распухнуть.
если сразу делать нормально (имеется в виду всегда возвращать промис из асинхронных операций), то потом не придется переделывать, чтобы успеть в дедлайны.
из асинхронных всегда возвращается промис.
Ну и по поводу вашей имплементации
Это просто минимальный пример, у меня на проде (как я уже упомянул) другой вариант без подобных проблем.
Я не предлагаю отказываться от промисов, это просто ещё один подход.
unel
06.10.2017 15:02Вообще между callback hell и промисами был ещё один промежуточный этап в виде использования либы async (или даже логичней дать ссылку на более старую версию)
Либа достаточно неплохо помогала бороться со всем этим callback-hell'ом и с неё было проще пересесть на промисы =) Не знаю даже, используют ли её сейчас или нет…
MrCheater
Хочу дополнить тему "Преобразование коллбэков в промисы"
Так как написано в статье, делать нынче уже не принято. Есть util.promisify