使用promises和递归

时间:2016-07-11 18:34:46

标签: javascript node.js recursion promise

我知道我在以下函数中提前返回,如何将递归promises链接到我的结果?

我的目标是获取目录中所有文件列表及其所有子目录。数组是单维的,我在这个例子中使用concat

function iterate(body) {
    return new Promise(function(resolve, reject){
        var list = [];
        fs.readdir(body.path, function(error, list){
            list.forEach(function(file){
                file = path.resolve(body.path, file);
                fs.stat(file, function(error, stat){
                    console.log(file, stat.isDirectory());
                    if(stat.isDirectory()) {
                        return iterate({path: file})
                            .then(function(result){
                                list.concat(result);
                            })
                            .catch(reject);
                    } else {
                        list.push(file);
                    }
                })
            });
            resolve(list);
        });
    });
};

1 个答案:

答案 0 :(得分:10)

There are numerous mistakes in your code. A partial list:

  1. .concat() returns a new array, so list.concat(result) by itself doesn't actually do anything.

  2. You're calling resolve() synchronously and not waiting for all async operations to be completed.

  3. You're trying to recursively return from deep inside several nested async callbacks. You can't do that. That won't get the results back anywhere.

I find this a ton easier to use by using a promisified version of the fs module. I use Bluebird to create that and then you can do this:

const path = require('path');
var Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

function iterate(dir) {
    return fs.readdirAsync(dir).map(function(file) {
        file = path.resolve(dir, file);
        return fs.statAsync(file).then(function(stat) {
            if (stat.isDirectory()) {
                return iterate(file);
            } else {
                return file;
            }
        })
    }).then(function(results) {
        // flatten the array of arrays
        return Array.prototype.concat.apply([], results);
    });
}

Note: I changed iterate() to just take the initial path so it's more generically useful. You can just pass body.path to it initially to adapt.


Here's a version using generic ES6 promises:

const path = require('path');
const fs = require('fs');

fs.readdirAsync = function(dir) {
    return new Promise(function(resolve, reject) {
        fs.readdir(dir, function(err, list) {
            if (err) {
                reject(err);
            } else {
                resolve(list);
            }
        });
    });
}

fs.statAsync = function(file) {
    return new Promise(function(resolve, reject) {
        fs.stat(file, function(err, stat) {
            if (err) {
                reject(err);
            } else {
                resolve(stat);
            }
        });
    });
}


function iterate2(dir) {
    return fs.readdirAsync(dir).then(function(list) {
        return Promise.all(list.map(function(file) {
            file = path.resolve(dir, file);
            return fs.statAsync(file).then(function(stat) {
                if (stat.isDirectory()) {
                    return iterate2(file);
                } else {
                    return file;
                }
            });
        }));
    }).then(function(results) {
        // flatten the array of arrays
        return Array.prototype.concat.apply([], results);
    });
}

iterate2(".").then(function(results) {
    console.log(results);
});

Here's a version that adds a customizable filter function:

function iterate2(dir, filterFn) {
    // default filter function accepts all files
    filterFn = filterFn || function() {return true;}
    return fs.readdirAsync(dir).then(function(list) {
        return Promise.all(list.map(function(file) {
            file = path.resolve(dir, file);
            return fs.statAsync(file).then(function(stat) {
                if (stat.isDirectory()) {
                    return iterate2(file, filterFn);
                } else {
                    return filterFn(file)? file : "";
                }
            });
        })).then(function(results) {
            return results.filter(function(f) {
                return !!f;
            });
        });
    }).then(function(results) {
        // flatten the array of arrays
        return Array.prototype.concat.apply([], results);
    });
}

// example usage
iterate2(".", function(f) {
    // filter out 
    return !(/(^|\/)\.[^\/\.]/g).test(f);
}).then(function(results) {
    console.log(results);
});