Итак вы открыли ноду и увидели, что почти все функции «из коробки» последним аргументом принимают колбэк.
var fs = require("fs");
fs.readdir(__dirname, function(error, files) {
if (error) {
console.error(error);
} else {
for (var i = 0, j = files.length; i < j; i++) {
console.log(files[i]);
}
}
});
Пирамида смерти
А в чем собственно проблема? Проблема в том, что на маке с ретиной порой заканчивается место под пробелы (конечно можно сказать, что 4 пробела на таб — роскошь) и весь код маячит далеко справа при использовании хотя бы десятка таких функций подряд.
var fs = require("fs");
var path = require("path");
var buffers = [];
fs.readdir(__dirname, function(error1, files) {
if (error1) {
console.error(error1);
} else {
for (var i = 0, j = files.length; i < j; i++) {
var file = path.join(__dirname, files[i]);
fs.stat(file, function(error2, stats) {
if (error2) {
console.error(error2);
} else if (stats.isFile()) {
fs.readFile(file, function(error3, buffer) {
if (error3) {
console.error(error3);
} else {
buffers.push(buffer);
}
});
}
});
}
}
});
console.log(buffers);
Так что же c этим можно сделать? Не применяя библиотек, для наглядности, так как с ними все примеры не займут и строчки кода, дальше будет показано как с этим справиться используя сахар es6 и es7.
Promise
Встроенный объект позволяющий немного разравнять пирамиду:
var fs = require("fs");
var path = require("path");
function promisify(func, args) {
return new Promise(function(resolve, reject) {
func.apply(null, [].concat(args, function(error, result) {
if (error) {
reject(error);
} else {
resolve(result);
}
}));
});
}
promisify(fs.readdir, [__dirname])
.then(function(items) {
return Promise.all(items.map(function(item) {
var file = path.join(__dirname, item);
return promisify(fs.stat, [file])
.then(function(stat) {
if (stat.isFile()) {
return promisify(fs.readFile, [file]);
} else {
throw new Error("Not a file!");
}
})
.catch(function(error) {
console.error(error);
});
}));
})
.then(function(buffers) {
return buffers.filter(function(buffer) {
return buffer;
});
})
.then(function(buffers) {
console.log(buffers);
})
.catch(function(error) {
console.error(error);
});
Кода стало немного больше, но зато сильно сократилась обработка ошибок.
Обратите внимание .catch был использован два раза потому, что Promise.all использует fail-fast стратегию и бросает ошибку, если ее бросил хотя бы один промис на практике такое пременение далеко не всегда оправдано, например если нужно проверить список проксей, то нужно проверить все, а не обламываться на первой «дохлой». Этот вопрос решают библиотеки Q и Bluebird и тд, поэтому его освещать не будем.
Теперь перепишем это все с учетом arrow functions, desctructive assignment и modules.
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
}]);
});
}
promisify(fs.readdir, [__dirname])
.then(items => Promise.all(items.map(item => {
const file = path.join(__dirname, item);
return promisify(fs.stat, [file])
.then(stat => {
if (stat.isFile()) {
return promisify(fs.readFile, [file]);
} else {
throw new Error("Not a file!");
}
})
.catch(console.error);
})))
.then(buffers => buffers.filter(e => e))
.then(console.log)
.catch(console.error);
Generator
Теперь совсем хорошо, но…ведь есть еще какие-то генераторы, которые добавляют новый тип функций function* и ключевое слово yeild, что будет если использовать их?
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
}]);
});
}
function getItems() {
return promisify(fs.readdir, [__dirname]);
}
function checkItems(items) {
return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
.then(stat => {
if (stat.isFile()) {
return file;
} else {
throw new Error("Not a file!");
}
})
.catch(console.error)))
.then(files => {
return files.filter(file => file);
});
}
function readFiles(files) {
return Promise.all(files.map(file => {
return promisify(fs.readFile, [file]);
}));
}
function * main() {
return yield readFiles(yield checkItems(yield getItems()));
}
const generator = main();
generator.next().value.then(items => {
return generator.next(items).value.then(files => {
return generator.next(files).value.then(buffers => {
console.log(buffers);
});
});
});
Цепочки из generator.next().value.then не лучше чем колбэки из первого примера однако это не значит, что генераторы плохие, они просто слабо подходят под эту задачу.
Async/Await
Еще два ключевых слова, с мутным значением, которые можно попробовать прилепить к решению, уже надоевшей задачи по чтению файлов- Async/Await
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}]);
});
}
function getItems() {
return promisify(fs.readdir, [__dirname]);
}
function checkItems(items) {
return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
.then(stat => {
if (stat.isFile()) {
return file;
} else {
throw new Error("Not a file!");
}
})
.catch(console.error)))
.then(files => {
return files.filter(file => file);
});
}
function readFiles(files) {
return Promise.all(files.map(file => {
return promisify(fs.readFile, [file]);
}));
}
async function main() {
return await readFiles(await checkItems(await getItems()));
}
main()
.then(console.log)
.catch(console.error);
Пожалуй самый красивый пример, все функции заняты своим делом и нету никаких пирамид.
Если писать этот код не для примера, то получилось бы как-то так:
import bluebird from "bluebird";
import fs from "fs";
import path from "path";
const myFs = bluebird.promisifyAll(fs);
function getItems(dirname) {
return myFs.readdirAsync(dirname)
.then(items => items.map(item => path.join(dirname, item)));
}
function getFulfilledValues(results) {
return results
.filter(result => result.isFulfilled())
.map(result => result.value());
}
function checkItems(items) {
return bluebird.settle(items.map(item => myFs.statAsync(item)
.then(stat => {
if (stat.isFile()) {
return [item];
} else if (stat.isDirectory()) {
return getItems(item);
}
})))
.then(getFulfilledValues)
.then(result => [].concat(...result));
}
function readFiles(files) {
return bluebird.settle(files.map(file => myFs.readFileAsync(file)))
.then(getFulfilledValues);
}
async function main(dirname) {
return await readFiles(await checkItems(await getItems(dirname)));
}
main(__dirname)
.then(console.log)
.catch(console.error);
Комментарии (44)
S3Ga
30.06.2016 14:18-13Кто нибудь скажет мне чем promise лучше «классического ajax»?
k12th
30.06.2016 14:28+15Тем же, чем мягкое лучше теплого.
На самом деле, профит от промизов в том, что это асинхронный компонуемый примитив. Они легко выстраиваются параллельно или последовательно или в любой комбинации, да еще обработка ошибок довольно удобная.
Справедливости ради, если не лепить анонимные функции, а грамотно все декомпозировать и разносить по модулям, то и без промисов все выглялит вполне прилично. Но промизы действительно легко компонуются.
tower120
30.06.2016 15:02+2Последняя версия кода с отдельными функциями сморится приятно, но как по мне изначальная версия гораздо понятнее.
А почему нельзя, например
if (error) { console.error(error); } else { for (var i = 0, j = files.length; i < j; i++) { console.log(files[i]); } }
Заменить на
if (error) { console.error(error); return; } for (var i = 0, j = files.length; i < j; i++) { console.log(files[i]); }
?
inoyakaigor
30.06.2016 18:24+1Предполагаю, потому, что этот код в цикле и выход из функции там не нужен. Всё-таки обрабатывается список файлов.
tower120
30.06.2016 18:48+1В примере все условия внутри лямбд.
fs.readdir(__dirname, function(error1, files) { if (error1) { console.error(error1); return; } var fs_stat = function(error2, stats) { if (error2) { console.error(error2); return; } if (!stats.isFile()) { return; } fs.readFile(file, function(error3, buffer) { if (error3) { console.error(error3); return; } buffers.push(buffer); }); }; for (var i = 0, j = files.length; i < j; i++) { var file = path.join(__dirname, files[i]); fs.stat(file, fs_stat); } });
По-моему вполне читаемо, и не размашисто.
Gryphon88
30.06.2016 15:27+4Проблема в том, что на маке с ретиной порой заканчивается место под пробелы
Приятно, что индустрия после первого шага «Оптимизировать дорого, докупим серверов» ещё не сделало второго «Если код содержит божественные объекты и избыточную вложенность, просто купить монитор с диагональю побольше»
kurtov
30.06.2016 16:11+1Без промисов использую такой подход:
$.post('/url', {data: 'data'}, onResponseCallback1); function onResponseCallback1() { $.post('/url', {data: 'data'}, onResponseCallback2); } function onResponseCallback2() { //... }
На ноде с промисами:
Promise.resolve() .then(onResponseCallback1) .then(onResponseCallback2) .catch(onError) function onResponseCallback1(data) { return data; } function onResponseCallback2(data) { return data; } // Для обработки ошибок function onError() {}
Никаких пирамид, функции можно делать чистыми, на мой взгляд более нагляден порядок выполнения. Возможно известная практика, на открытие Америки не претендую.
Alexey2005
30.06.2016 16:39Первый пример (который без промисов) точно будет работать так, как задумано? Ведь fs.readdir асинхронна, так что помешает console.log выполниться сразу после неё, до того, как управление попадёт в коллбэк? Тогда в логе окажется пустой массив.
Ну и помимо указанных способов можно делать так, как обычно делают все новички в JS, впервые столкнувшиеся с асинхронной лапшой — т.е. поименовать все коллбэки, расположив их друг за другом (выше уже сказали о такой возможности):
fs.readdir(__dirname, processFiles ); function processFiles(error1, files) { if (error1) { console.error(error1); } else { beginReading(files); }; } function beginReading(files) { for (var i = 0, j = files.length; i < j; i++) { var file = path.join(__dirname, files[i]); fs.stat(file, getFileStats); } } function getFileStats(error2, stats) { if (error2) { console.error(error2); } else if (stats.isFile()) { fs.readFile(file, fileReaded); } }; function fileReaded(error3, buffer) { if (error3) { console.error(error3); } else { buffers.push(buffer); } }
Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти. Вроде и инструментов куча, чтоб этого избежать, и подходов множество, а всё равно на мало-мальски большом проекте оно проявляется.TrejGun
30.06.2016 17:36да `buffers` будет пустой, он там для того что бы показать куда попали данные, иначе непонятно что делает код, так как это даже не функция
поименовать колбеки, кстати полезно не только новичкам, но и для отладки, особенно если код был пропущен через babel
gearbox
30.06.2016 16:56-1Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти.
Ну не знаю — ни в js ни в typescript не наблюдаю ни лапши ни пирамид. А еще в копилку перечисленного — @autobind декоратор, помогает при использовании методов класса в качестве коллбэков.
TrejGun
30.06.2016 17:36-1объясните чем хорошо @autobind. то есть я понимаю его смысл и зачем он нужен
однако в текущей реализации он ущербен https://github.com/jayphelps/core-decorators.js/issues/76
а для того что бы использовать его в лоб достаточно двойного двоеточия `::obj.method` (вот же ж этот камент про пхп сверху)gearbox
30.06.2016 18:51-1Вы:
однако в текущей реализации он ущербен
@autobind doesn't work properly when calling super from the parent's parent's parent.
@autobind
class A {
method() {
console.log(this.test);
}
}
Я:
@autobind декоратор, помогает при использовании методов класса в качестве коллбэков.
Так то никто Вам не мешает сальто в прыжке с переворотом делать, просто я то не об этом говорил.
TrejGun
30.06.2016 21:19-1а я как раз об этом — для использования «в лоб»
promise.then(::obj.method)
достаточно встроенного сахара и не нужно подключать кучу дополнительных либgearbox
30.06.2016 21:35можно. Но если метод класса изначально предназначен для использования коллбэком (на всякие addListener) — то проще поставить один раз декоратор на метод в описании и не парится каждый раз по поводу того как его добавлять в хуки.
KonstantinSoloviov
30.06.2016 17:21Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти.
Ну, не знаю — у меня в таком знаете ли нефиговом проекте на С++ (24*7) утечек памяти не наблюдается
А вообще, это же обычная прогулка через «тернии к звездам» когда успешный сценарий один, а возможных ошибок мильон:
bool bOk = false;
do{
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
if(что-то не так) break;
bOk = true;
}while(0);
if(bOk) все получилось;
else обрабатываем ошибку;
Коды ошибок и выводы в лог — добавляются по вкусу.
В JS так нельзя?k12th
30.06.2016 17:52+1Можно, конечно! Если это синхронные операции.
Поскольку многопоточности в JS нету, а вешать весь браузер на каждый ajax-запрос никому не хочется, выкручиваемся асинхронностью — запрашиваем операцию и передаем ссылку на функцию, которую надо вызвать по ее окончании. И вот вроде бы и параллельность есть и программист себе последнюю ногу не отстрелил (ох уж эти программисты, на что только не идут, лишь бы не чинить баг в race condition).
Вот только если наспех код херачить, то вот такая «пирамидка» получается.vintage
30.06.2016 17:58k12th
30.06.2016 18:03+1Да я и не спорю. Просто объяснил товарищу, как мы тут в JS живем.
KonstantinSoloviov
30.06.2016 20:48+2Спасибо, друзья! Реально — интересно. Ощущение, что «на чужой раён» забрел, но любопытно же )
Поскольку многопоточности в JS нету, а вешать весь браузер на каждый ajax-запрос никому не хочется, выкручиваемся асинхронностью — запрашиваем операцию и передаем ссылку на функцию, которую надо вызвать по ее окончании.
Это понятно (на LUA похоже, там сплошь и рядом), но где, я извиняюсь, в Вашей ассинхронности таймауты? Про точки синхронизации даже не спрашиваю пока. Неужели сам браузер их определяет?k12th
30.06.2016 20:56+2Да, их определяет среда выполнения. Если вы сделаете
setTimeout(myFunc, 1000)
, нет никакой гарантии, что myFunc выполнится через тысячу миллисекунд. Если в этот момент браузер (или nodejs) будет занят чем-то другим (например, обработкой какого-то события от пользователя), то «пусть весь мир подождет». Есть некоторый стэк вызовов, если он не пуст, то наш коллбэк вызовется, только когда он очистится.
Antelle
30.06.2016 17:56Это называется resumable function. В С++ по этой же причине ввели future (=js promise) и вводят async/await.
vintage
30.06.2016 17:33-2Информация для медитации: https://github.com/nin-jin/async-js/
Пулреквесты с реализацией на генераторах и асинкавайте приветствуются :-)vintage
01.07.2016 07:33Кстати, первый код в посте и в комментариях — препаршивый, так как размазывает обработку ошибок по всему коду, вместо того, чтобы пробрасывать ошибку наверх и обрабатывать ошибки в одном месте. Вариант с промисами этого бага лишён.
Правильная обработка ошибок в ноде выглядит так:
var myAsyncFunction = ( ...args , done ) => { otherAsyncFunction( ( error, result1 ) => { if( error ) return done( error ) // do something with result1 and generate result2 done( null , result2 ) } ) }
Вы бы не минусы ставили, а сходили по ссылке и посмотрели правильные реализации на разных подходах.
Grammka
30.06.2016 18:04для генераторов заюзать что-то типа CO и будет тоже самое что с и async/await… это к слову про «это самый красивый подход»… А разве async/await поддерживается в NodeJS?
TrejGun
30.06.2016 18:16то есть пирамиду из `generator.next().value.then` замели под ковер под названием СО и сказали, что это тру :)
Grammka
30.06.2016 18:18Ну если вы используете генераторы, то вы явно будете использовать «СО»… это можно рассматривать как подключение любого стороннего модуля, так что да — это нормально, ИМХО. А как вы рассуждаете можно и кишки async/await наружу вытащить =)
TrejGun
30.06.2016 18:37+1согласен если статья приводит пример с использованием `bluebird` то пример с `CO` ничем не хуже.
а вот кишки async/await написаны на С++ так что сравнивать их можно только с чем-то подобным, например с https://github.com/SyntheticSemantics/ems
Alternator
30.06.2016 23:52Используя транспайлеры(например babel) вполне поддерживается, причем практически в любом окружении.
Более того, с помощью babel-а, можно использовать и кучу других синтаксических плюшек из будущих стандартов и даже то, что не планируется к стандартизации в языке(например react, flow)
richtrr
30.06.2016 23:44+1не могу смотреть как юзается переменная «file» в первой версии кода, — какое по вашему у неё будет значение во время выполнения fs.readFile(file, ...)? остальные версии вообще не понял, должно быть старею ))
не пойму почему так не написать:
function isNotError(err) { if(!err) return true; console.error(err); return false; } function onReadDir(err, files) { if(isNotError(err)) { for (var i = 0; i < files.length; i++) readFile(path.join(__dirname, files[i])); } } function readFile(file) { function onStat(err, stats) { if(isNotError(err) && stats.isFile()) fs.readFile(file, onRead); } function onRead(err, buffer) { if(isNotError(err)) buffers.push(buffer); } fs.stat(file, onStat); } fs.readdir(__dirname, onReadDir);
TrejGun
01.07.2016 02:41это вы не можете смотреть?
после вот этого?
function isNotError(err) { if(!err) return true; console.error(err); return false; }
richtrr
01.07.2016 09:42вы так говорите, будто работающий код хуже неработающего. что-то я не понял к чему ваше замечание.
но признаю, с «не могу смотреть» я, пожалуй, перегнул, — напротив, мне просто бальзам на душу, когда кто-то начинает устраивать замыкания внутри цикла, за что получает трудно уловимый баг, — тогда я смотрю на это высунув язык от радости и приговариваю: «я же говорил, что не надо использовать анонимные функции где попало».
standy
01.07.2016 04:18+1У вас async/await код больно страшный. К примеру, в функции
checkItems
последниеcatch
иthen
на одном уровне, хотя первое относится кfs.stat
, а второе кPromise.all
. Плюс использовать исключения для управления потоком не очень хорошо
К тому же код в примерах не эквивалентный, в promise-версии файлы фильтруются и читаются сразу, а в async-версии сначала получается список файлов, потом читается по списку — отсюда два использования
Promise.all
У меня с async получилось так:
async function readFile(file) { const stat = await promisify(fs.stat, [path.join(__dirname, file)]); if (!stat.isFile()) { console.error("Not a file!"); return null; } return await promisify(fs.readFile, [file]); } async function main() { const items = await promisify(fs.readdir, [__dirname]); const buffersOrNull = await Promise.all(items.map(readFile)); return buffersOrNull.filter(buffer => buffer !== null); }
YourChief
08.07.2016 22:47Третий случай, замеченный за корпоративным блогом ua-hosting, когда они копипастят текст с другого ресурса.
На этот раз это даже не перевод: http://mabp.kiev.ua/2015/12/24/pyramid-of-death/TrejGun
08.07.2016 23:20Да, Таша с моего разрешения опубликовала статью, поскольку у меня не хватает кармы, что бы сделать это самостоятельно
Как видите, я активно отвечаю на каменты, так что, пожалуйста, не надо делать из этого поста корпоративные войны
Zenitchik
У меня велосипед получился точно такой же. С точностью до названий функций. А переименую-ка я их как у Вас...