程序失败,因为readFile是异步的?

时间:2016-05-28 15:23:00

标签: javascript node.js

我试图合并两个列表。 List1有681个法语动词,List2有681个翻译。 我使用javascript和node.js来读取文件。 这是我的尝试:

    var frenchWords, englishWords, combinedList;
    fs = require('fs')

// 1. read the french file

    fs.readFile('frenchVerbsList.txt', 'utf8', function (err,data) {
    if (err) {
        return console.log("ERROR here!: " + err);
    }
    frenchWords = data.split('\n');
    });

//read the english file

    fs.readFile('englishVerbsList.txt', 'utf8', function (err,data2) {
    if (err) {
        return console.log("ERROR here!: " + err);
    }
    englishWords = data2.split('\n');
    });

// 2. combine the lists
//*** it fails here, I'm guessing it's because the readFile operation hasn't finished yet.

    var combinedList;
    for(i=0; i<frenchWords.length; i++){
        combinedList[i] = frenchWords[i] + ",,," + englishWords[i];
    }
// 3. check the result

    for(i=0; i<10; i++){
        console.log(combinedList[i]);
    }

非常感谢您的帮助,我这样做是为了让我保持活跃:-)

3 个答案:

答案 0 :(得分:1)

使用某个库(例如async)等待两个任务(读取文件)完成然后继续。

或者使用readFileSync代替(但在大多数情况下首选使用异步版本,除非它只是一个本地“工具”脚本,而不是服务器的一部分)。

答案 1 :(得分:1)

你的猜测是正确的,当你到达代码的组合部分时,没有什么可以保证文件读取完成,并且没有任何承诺两个文件以相同的速度处理。

您需要做的是确保在合并列表之前完成这两个文件。您可以使用async库来完成此操作。你在其中的一个函数是parallel,它可以除了两个函数(在你的情况下,每个文件一个)并且并行运行它们,并且第三个回调函数将在两个函数完成后执行

答案 2 :(得分:1)

你是正确的fs.readFile()回调的异步性质导致了你的问题。

将来,在其余代码继续运行时,将在不确定的时间调用这些回调。由于node.js的事件驱动设计,在代码的其余部分完成执行之前,不会调用回调。因此,我们保证您会在englishWordsfrenchWords变量有任何结果之前尝试使用它们。

你有很多不同的选择:

切换到fs.readFileAsync(大多数情况下不推荐)

您可以切换为使用fs.readFileSync()。这是最简单的更改,因为您当前的控制流程将起作用。但是,这通常不建议用于node.js开发,因为它对服务器使用效率低下。如果此代码位于服务器进程中,可以/应该保持在等待读取文件时执行其他操作的能力,那么fs.readFileSync()将会破坏可伸缩性。如果这只是一个一次性脚本(不是加载的服务器),那么fs.readFileSync()可能会正常工作。但是,你应该学习更好的&#34; node.js-style&#34;等待使用异步操作正确编码(请参阅以下选项)。

通过继续回调内部流程序列化操作

您可以使用嵌套来序列化异步操作。这涉及仅在异步回调中继续处理逻辑。这样,您就知道您需要的结果可供您继续处理。这看起来像这样:

const fs = require('fs')

// read the french file
fs.readFile('frenchVerbsList.txt', 'utf8', function (err, data) {
    if (err) {
        return console.log("ERROR here!: " + err);
    }
    var frenchWords = data.split('\n');

    //read the english file
    fs.readFile('englishVerbsList.txt', 'utf8', function (err, data2) {
        if (err) {
            return console.log("ERROR here!: " + err);
        }
        var englishWords = data2.split('\n');

        // 2. combine the lists
        var combinedList = [];
        for (i = 0; i < frenchWords.length; i++) {
            combinedList[i] = frenchWords[i] + ",,," + englishWords[i];
        }

        // 3. check the result
        for (i = 0; i < 10; i++) {
            console.log(combinedList[i]);
        }
    });
});

手动编码检查两个异步操作何时完成

上面的序列化选项的缺点是它在开始下一个异步操作之前等待第一次异步操作。这不太理想,因为异步操作可以并行运行(更快的最终结果)。以下所有选项将以不同的方式并行运行异步操作,并监视它们何时完成,以便您可以触发最终处理。这是手动监控选项。在加载法语和英语单词的完成回调中,你检查另一个是否也完成了。如果是,则调用函数来处理结果。由于只有一个可以一次完成,只要没有错误,其中一个将完成第二个并将调用您的函数来处理结果:

var frenchWords, englishWords;
fs = require('fs')

// read the french file
fs.readFile('frenchVerbsList.txt', 'utf8', function (err, data) {
    if (err) {
        return console.log("ERROR here!: " + err);
    }
    frenchWords = data.split('\n');
    if (frenchWords && englishWords) {
        processResults();
    }
});

//read the english file

fs.readFile('englishVerbsList.txt', 'utf8', function (err, data2) {
    if (err) {
        return console.log("ERROR here!: " + err);
    }
    englishWords = data2.split('\n');
    if (frenchWords && englishWords) {
        processResults();
    }
});

function processResults() {

    // combine the lists
    var combinedList = [];
    for (let i = 0; i < frenchWords.length; i++) {
        combinedList[i] = frenchWords[i] + ",,," + englishWords[i];
    }

    // check the result
    for (let i = 0; i < 10; i++) {
        console.log(combinedList[i]);
    }
}

使用ES6承诺监控异步操作

使用ES6,promises现在已经成为Javascript规范的标准部分,它们是协调多个异步操作的绝佳方式,它们也使得正确的错误处理(特别是在复杂情况下)更加直接。要在这里使用承诺,您首先要创建一个&#34; promisified&#34;版本fs.readFile()。这将是一个使用promises而不是普通回调的包装函数。然后,您可以使用Promise.all()来协调两个异步操作的完成时间。

var fs = require('fs');
// promise wrapper
fs.readFileAsync = function(file, encoding) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, encoding, function(err, data) {
            if (err) return reject(err);
            resolve(data);
        });        
    });
}

// common helper function
function readFileSplitWords(file) {
    return fs.readFileAsync(file, 'utf8').then(function(data) {
        // make split words be the fulfilled value of the promise
        return data.split('\n');
    });
}

var frenchPromise = readFileSplitWords('frenchVerbsList.text');
var englishPromise = readFileSplitWords('englishVerbsList.txt');
Promise.all([frenchPromise, englishPromise]).then(function(results) {
    // combine the lists
    var frenchWords = results[0], englishWords = results[1];
    var combinedList = [];
    for (i = 0; i < frenchWords.length; i++) {
        combinedList[i] = frenchWords[i] + ",,," + englishWords[i];
    }

    // check the result
    for (i = 0; i < 10; i++) {
        console.log(combinedList[i]);
    }
}, function(err) {
   // handle an error here
});

使用承诺库实现扩展承诺功能

ES6 Promises非常强大,但在使用某些第三方库添加的承诺时,有非常有用的功能。我个人使用Bluebird库。以下是使用Bluebird库的上一个选项的外观:

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

// common helper function
function readFileSplitWords(file) {
    return fs.readFileAsync(file, 'utf8').then(function(data) {
        // make split words be the fulfilled value of the promise
        return data.split('\n');
    });
}

var frenchPromise = readFileSplitWords('frenchVerbsList.text');
var englishPromise = readFileSplitWords('englishVerbsList.txt');
Promise.all([frenchPromise, englishPromise]).spread(function(frenchWords, englishWords) {
    // combine the lists
    var combinedList = [];
    for (i = 0; i < frenchWords.length; i++) {
        combinedList[i] = frenchWords[i] + ",,," + englishWords[i];
    }

    // check the result
    for (i = 0; i < 10; i++) {
        console.log(combinedList[i]);
    }
}, function(err) {
   // handle an error here
});

这使用Bluebird的Promise.promisifyAll()自动制作fs库中所有方法的预定版本(非常有用)。并且,它使用.spread()方法而不是.then()来自动将两个结果分离为其命名参数。

使用更多扩展功能处理任意数组文件名

您还可以使用更多扩展的Bluebird功能,例如处理数组的Promise.map(),然后对结果承诺执行Promise.all()(上述代码手动完成)。然后,这允许您使文件名成为您想要的任何语言的文件名的任意列表,并且在这方面可以使代码更通用:

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

// common helper function
function readFileSplitWords(file) {
    return fs.readFileAsync(file, 'utf8').then(function(data) {
        // make split words be the fulfilled value of the promise
        return data.split('\n');
    });
}

var files = ['frenchVerbsList.text', 'englishVerbsList.txt'];
Promise.map(files, readFileSplitWords).then(function(results) {
    // results is an array of arrays where each sub-array is a language list of words
    // combine the lists (assumes all word lists have the same length)
    var combinedList = [];
    var len = results[0].length;
    // for each word in the first array
    for (var i = 0; i < len; i++) {
        // get all the other words in the same array position
        var words = [];
        for (var j = 0; j < results.length; j++) {
            words.push(results[j][i]);
        }
        combinedList.push(words.join(',,,'));
    }

    // check the result
    for (i = 0; i < 10; i++) {
        console.log(combinedList[i]);
    }
}, function(err) {
   // handle an error here
});