Настроить webpack по мануалу, запрограммировать ангуляр и даже послать json по ajax — кажись каждый может, но вот как взглянешь на сам код… В этом посте будет показана разница между нововведениями.

Итак вы открыли ноду и увидели, что почти все функции «из коробки» последним аргументом принимают колбэк.

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)


  1. Zenitchik
    30.06.2016 14:18

    У меня велосипед получился точно такой же. С точностью до названий функций. А переименую-ка я их как у Вас...


  1. S3Ga
    30.06.2016 14:18
    -13

    Кто нибудь скажет мне чем promise лучше «классического ajax»?


    1. k12th
      30.06.2016 14:28
      +15

      Тем же, чем мягкое лучше теплого.

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

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


  1. TashaFridrih
    30.06.2016 14:25
    +6

    это не из разряда лучше, а скорее вместе


  1. 7ide
    30.06.2016 14:42

    github.com/yortus/asyncawait — замечательная либа


  1. 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]);
        }

    ?


    1. inoyakaigor
      30.06.2016 18:24
      +1

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


      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);
            }
        });

        По-моему вполне читаемо, и не размашисто.


  1. Gryphon88
    30.06.2016 15:27
    +4

    Проблема в том, что на маке с ретиной порой заканчивается место под пробелы

    Приятно, что индустрия после первого шага «Оптимизировать дорого, докупим серверов» ещё не сделало второго «Если код содержит божественные объекты и избыточную вложенность, просто купить монитор с диагональю побольше»


  1. 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() {}
    


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


    1. vtrushin
      30.06.2016 16:26

      Какие же они чистые, если в них вызывается асинхронный код и они при этом сами ничего не возвращают


      1. kurtov
        30.06.2016 16:42
        -1

        А я и не говорил, что они чистые.


  1. yociyavi
    30.06.2016 16:18
    -7

    Переходите на php, и забудьте про эти костыли


    1. Prototik
      30.06.2016 20:33
      +3

      … и вспомните про новые, более костыльные костыли, нежели эти ваши асинхронные костыли.


  1. 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++ утечки памяти. Вроде и инструментов куча, чтоб этого избежать, и подходов множество, а всё равно на мало-мальски большом проекте оно проявляется.


    1. TrejGun
      30.06.2016 17:36

      да `buffers` будет пустой, он там для того что бы показать куда попали данные, иначе непонятно что делает код, так как это даже не функция

      поименовать колбеки, кстати полезно не только новичкам, но и для отладки, особенно если код был пропущен через babel


  1. gearbox
    30.06.2016 16:56
    -1

    Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти.

    Ну не знаю — ни в js ни в typescript не наблюдаю ни лапши ни пирамид. А еще в копилку перечисленного — @autobind декоратор, помогает при использовании методов класса в качестве коллбэков.


    1. TrejGun
      30.06.2016 17:36
      -1

      объясните чем хорошо @autobind. то есть я понимаю его смысл и зачем он нужен
      однако в текущей реализации он ущербен https://github.com/jayphelps/core-decorators.js/issues/76
      а для того что бы использовать его в лоб достаточно двойного двоеточия `::obj.method` (вот же ж этот камент про пхп сверху)


      1. 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 декоратор, помогает при использовании методов класса в качестве коллбэков.


        Так то никто Вам не мешает сальто в прыжке с переворотом делать, просто я то не об этом говорил.


        1. TrejGun
          30.06.2016 21:19
          -1

          а я как раз об этом — для использования «в лоб»

          promise.then(::obj.method)
          


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


          1. gearbox
            30.06.2016 21:35

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


  1. 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 так нельзя?


    1. k12th
      30.06.2016 17:52
      +1

      Можно, конечно! Если это синхронные операции.

      Поскольку многопоточности в JS нету, а вешать весь браузер на каждый ajax-запрос никому не хочется, выкручиваемся асинхронностью — запрашиваем операцию и передаем ссылку на функцию, которую надо вызвать по ее окончании. И вот вроде бы и параллельность есть и программист себе последнюю ногу не отстрелил (ох уж эти программисты, на что только не идут, лишь бы не чинить баг в race condition).
      Вот только если наспех код херачить, то вот такая «пирамидка» получается.


      1. vintage
        30.06.2016 17:58

        1. k12th
          30.06.2016 18:03
          +1

          Да я и не спорю. Просто объяснил товарищу, как мы тут в JS живем.


          1. KonstantinSoloviov
            30.06.2016 20:48
            +2

            Спасибо, друзья! Реально — интересно. Ощущение, что «на чужой раён» забрел, но любопытно же )

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


            1. k12th
              30.06.2016 20:56
              +2

              Да, их определяет среда выполнения. Если вы сделаете setTimeout(myFunc, 1000), нет никакой гарантии, что myFunc выполнится через тысячу миллисекунд. Если в этот момент браузер (или nodejs) будет занят чем-то другим (например, обработкой какого-то события от пользователя), то «пусть весь мир подождет». Есть некоторый стэк вызовов, если он не пуст, то наш коллбэк вызовется, только когда он очистится.


    1. Antelle
      30.06.2016 17:56

      Это называется resumable function. В С++ по этой же причине ввели future (=js promise) и вводят async/await.


  1. vintage
    30.06.2016 17:33
    -2

    Информация для медитации: https://github.com/nin-jin/async-js/
    Пулреквесты с реализацией на генераторах и асинкавайте приветствуются :-)


    1. 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 )
          } )
      }

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


  1. roman12rus
    30.06.2016 18:00

    Вот спасибо. Как раз сейчас с колбэками в JS разбираюсь.


  1. Grammka
    30.06.2016 18:04

    для генераторов заюзать что-то типа CO и будет тоже самое что с и async/await… это к слову про «это самый красивый подход»… А разве async/await поддерживается в NodeJS?


    1. TrejGun
      30.06.2016 18:16

      то есть пирамиду из `generator.next().value.then` замели под ковер под названием СО и сказали, что это тру :)


      1. Grammka
        30.06.2016 18:18

        Ну если вы используете генераторы, то вы явно будете использовать «СО»… это можно рассматривать как подключение любого стороннего модуля, так что да — это нормально, ИМХО. А как вы рассуждаете можно и кишки async/await наружу вытащить =)


        1. TrejGun
          30.06.2016 18:37
          +1

          согласен если статья приводит пример с использованием `bluebird` то пример с `CO` ничем не хуже.

          а вот кишки async/await написаны на С++ так что сравнивать их можно только с чем-то подобным, например с https://github.com/SyntheticSemantics/ems


    1. Alternator
      30.06.2016 23:52

      Используя транспайлеры(например babel) вполне поддерживается, причем практически в любом окружении.
      Более того, с помощью babel-а, можно использовать и кучу других синтаксических плюшек из будущих стандартов и даже то, что не планируется к стандартизации в языке(например react, flow)


  1. 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);
    


    1. TrejGun
      01.07.2016 02:41

      это вы не можете смотреть?
      после вот этого?

      function isNotError(err) {
          if(!err) 
              return true;
          console.error(err);
          return false;
      }
      


      1. richtrr
        01.07.2016 09:42

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


  1. 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);
    }


  1. YourChief
    08.07.2016 22:47

    Третий случай, замеченный за корпоративным блогом ua-hosting, когда они копипастят текст с другого ресурса.

    На этот раз это даже не перевод: http://mabp.kiev.ua/2015/12/24/pyramid-of-death/


    1. TrejGun
      08.07.2016 23:20

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

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


      1. YourChief
        08.07.2016 23:25
        -2

        Добрый день! Тогда это кросспост, который запрещён правилами ;-)


        1. TrejGun
          08.07.2016 23:29
          +1

          Добрый пятничный вечер :)
          несовсем, это адаптированая, для широких масс, версия оригинала
          http://pyha.ru/forum/topic/9319.1