Node Pattern: Asynchronous Loops

Un tipo di problematica ricorrente in node.js riguarda chiamate di funzioni non bloccanti (asincrone) in un ciclo, e dove il risultato dovrà essere utilizzato nel codice chiamante tramite una callback.

Facciamo il seguente esempio:

function load_album_list (callback) {
    fs.readdir("albums",
        function (err, files) {
            if (err) {
                return callback(err);
            }
            var only_dirs = [];
            for (var i = 0; i < files.length; i++) {
                fs.stat("albums/" + files[i],
                    function (err, stats) {
                        if (stats.isDirectory()) {
                            only_dirs.push(files[i]);
                        }
                    });
            }
            callback(null, only_dirs);
        });
}

Se si esegue il codice illustrato sopra si ottiene il seguente valore:
{“error”:null,”data”:{“albums”:[]}}

Il motivo è molto semplice:
Dopo l’avvio di tutte queste funzioni non bloccante, si esce dal ciclo e si chiama la callback passata come parametro. Poiché node.js è single threaded, nessuna delle funzioni fs.stat avrà avuto la possibilità di essere eseguite e chiamare le loro callback, così only_dirs è vuoto quando utilizzato.

Vi sono vari modi per risolvere questo problema.
Quello più immediato fa uso di contatori nel ciclo, ma non è molto elegante, ed è potenzialmente rischioso.
Una soluzione più elegante utilizza funzioni ricorsive con il seguente formato:

function iterator (i) {
    if( i < array.length ) {
        async_work( function(){
            iterator( i + 1 )
        })
    } else {
        callback(results);
    }
}
iterator(0)

Si può ulteriormente migliorare questo codice utilizzando una funzione anonima denominata al fine di non ingombrare lo scope con nomi di funzioni inutili al resto del codice:

(function iterator (i) {
    if( i < array.length ) {
        async_work( function(){
            iterator( i + 1 )
        })
    } else {
        callback(results);
    }
})(0);

Quindi potremmo riscrivere il codice precedente come segue:

function load_album_list (callback) {
    fs.readdir("albums",
        function (err, files) {
            if (err) {
                return callback(err);
            }
            var only_dirs = [];
            ( function iterator(index) {
                if (index == files.length) {
                    return callback(null, only_dirs);
                }
                fs.stat("albums/" + files[index],
                    function (err, stats) {
                        if (err) {
                            return callback(err);
                        }
                        if (stats.isDirectory()) {
                            only_dirs.push(files[index]);
                        }
                        iterator(index + 1)
                    }
                );
            })(0);
        }
    );
}


So, what do you think ?