简体   繁体   中英

How to wrangle Node.JS async

I am struggling with getting my head around how to overcome and handle the async nature of Node.JS. I have done quite a bit of reading on it and tried to make Node do what I want by either using a message passing solution or callback functions.

My problem is I have a object where I want to constructor to load a file and populate an array. Then I want all calls to this function use that loaded data. So I need the original call to wait for the file to be loaded and all subsequent calls to use the already loaded private member.

My issue is that the function to load load the data and get the data is being executed async even if it return a function with a callback.

Anyways, is there something simple I am missing? Or is there an easier pattern I could use here? This function should return part of the loaded file but returns undefined. I have checked that the file is actually being loaded, and works correctly.

function Song() {
    this.verses = undefined;

    this.loadVerses = function(verseNum, callback) {
        if (this.verses === undefined) {
            var fs = require('fs'),
                filename = 'README.md';

            fs.readFile(filename, 'utf8', function(err, data) {
                if (err) {
                    console.log('error throw opening file: %s, err: %s', filename, err);
                    throw err;
                } else {
                    this.verses = data;
                    return callback(verseNum);
                }
            });
        } else {
            return callback(verseNum);
        }
    }

    this.getVerse = function(verseNum) {
        return this.verses[verseNum + 1];
    }
}

Song.prototype = {
    verse: function(input) {
        return this.loadVerses(input, this.getVerse);
    }
}

module.exports = new Song();

Update:

This is how I am using the song module from another module

var song = require('./song');
return song.verse(1);

"My issue is that the function to load the data and get the data is being executed async even if it return a function with a callback."

@AlbertoZaccagni what I mean by that scentence is that this line return this.loadVerses(input, this.getVerse); returns before the file is loaded when I expect it to wait for the callback.

That is how node works, I will try to clarify it with an example.

function readFile(path, callback) {
  console.log('about to read...');
  fs.readFile(path, 'utf8', function(err, data) {
    callback();
  });
}

console.log('start');
readFile('/path/to/the/file', function() {
  console.log('...read!');
});
console.log('end');

You are reading a file and in the console you will likely have

  • start
  • about to read...
  • end
  • ...read!

You can try that separately to see it in action and tweak it to understand the point. What's important to notice here is that your code will keep on running skipping the execution of the callback, until the file is read.

Just because you declared a callback does not mean that the execution will halt until the callback is called and then resumed.

So this is how I would change that code:

function Song() {
  this.verses = undefined;

  this.loadVerses = function(verseNum, callback) {
    if (this.verses === undefined) {
      var fs = require('fs'),
      filename = 'README.md';
      fs.readFile(filename, 'utf8', function(err, data) {
        if (err) {
          console.log('error throw opening file: %s, err: %s', filename, err);
          throw err;
        } else {
          this.verses = data;
          return callback(verseNum);
        }
      });
    } else {
      return callback(verseNum);
    }
  }
}

Song.prototype = {
  verse: function(input, callback) {
    // I've removed returns here
    // I think they were confusing you, feel free to add them back in
    // but they are not actually returning your value, which is instead an
    // argument of the callback function
    this.loadVerses(input, function(verseNum) {
      callback(this.verses[verseNum + 1]);
    });
  }
}

module.exports = new Song();

To use it:

var song = require('./song');
song.verse(1, function(verse) {
  console.log(verse);
});

I've ignored

  1. the fact that we're not treating the error as first argument of the callback
  2. the fact that calling this fast enough will create racing conditions, but I believe this is another question

[Collected into an answer and expanded from my previous comments]

TL;DR You need to structure your code such that the result of any operation is only used inside that operation's callback, since you do not have access to it anywhere else.

And while assigning it to an external global variable will certainly work as expected, do so will only occur after the callback has fired, which happens at a time you cannot predict .

Commentary

Callbacks do not return values because by their very nature, they are executed sometime in the future.

Once you pass a callback function into a controlling asynchronous function, it will be executed when the surrounding function decides to call it. You do not control this, and so waiting for a returned result won't work.

Your example code, song.verse(1); cannot be expected to return anything useful because it is called immediately and since the callback hasn't yet fired, will simply return the only value it can: null .

I'm afraid this reliance on asynchronous functions with passed callbacks is an irremovable feature of how NodeJS operates; it is at the very core of it .

Don't be disheartened though. A quick survey of all the NodeJS questions here shows quite clearly that this idea that one must work with the results of async operations only in their callbacks is the single greatest impediment to anyone understanding how to program in NodeJS.

For a truly excellent explanation/tutorial on the various ways to correctly structure NodeJS code, see Managing Node.js Callback Hell with Promises, Generators and Other Approaches .

I believe it clearly and succinctly describes the problem you face and provides several ways to refactor your code correctly.

Two of the features mentioned there, Promises and Generators, are programming features/concepts, the understanding of which would I believe be of great use to you.

Promises (or as some call them, Futures ) is/are a programming abstraction that allows one to write code a little more linearly in a if this then that style, like

fs.readFileAsync(path).then(function(data){ 

    /* do something with data here */
    return result;

}).catch(function(err){

    /* deal with errors from readFileAsync here */

}).then(function(result_of_last_operation){ 

    /* do something with result_of_last_operation here */

    if(there_is_a_problem) throw new Error('there is a problem');

    return final_result;

})
.catch(function(err){

    /* deal with errors when there_is_a_problem here */

}).done(function(final_result){

    /* do something with the final result */

});

In reality, Promises are simply a means of marshaling the standard callback pyramid in a more linear fashion. (Personally I believe they need a new name, since the idea of "a promise of some value that might appear in the future" is not an easy one to wrap one's head around, at least it wasn't for me.)

Promises do this by (behind the scenes) restructuring "callback hell" such that:

asyncFunc(args,function callback(err,result){
   if(err) throw err;
   /* do something with the result here*/
});

becomes something more akin to:

var p=function(){ 
    return new Promise(function(resolve,reject){
        asyncFunc(args,function callback(err,result){
            if(err) reject(err)
            resolve(result);
        });
    });
});

p();

where any value you provide to resolve() becomes the only argument to the next "then-able" callback and any error is passed via rejected() , so it can be caught by any .catch(function(err){ ... }) handlers you define.

Promises also do all the things you'd expect from the (somewhat standard) async module, like running callbacks in series or in parallel and operating over the elements of an array, returning their collected results to a callback once all the results have been gathered.

But you will note that Promises don't quite do what you want, because everything is still in callbacks.

(See bluebird for what I believe is the simplest and thus, best Promises package to learn first.)

(And note that fs.readFileAsync is not a typo. One useful feature of bluebird is that it can be made to add this and other Promises-based versions of fs 's existing functions to the standard fs object. It also understands how to "promisify" other modules such as request and mkdirp ).

Generators are the other feature described in the tutorial above, but are only available in the new, updated but not yet officially released version of JavaScript (codenamed "Harmony").

Using generators would also allow you to write code in a more linear manner, since one of the features it provides is the ability of waiting on the results of an asynchronous operation in a way that doesn't wreak havoc with the JavaScript event-loop. (But as I said, it's not a feature in general use yet.)

You can however use generators in the current release of node if you'd like, simply add "--harmony" to the node command line to tell it to turn on the newest features of the next version of JavaScript.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM