My First Async I/O! introduces us to asynchronous input/output. The node.js framework is built on the idea of using asynchronous operations rather than synchronous operations like we saw in lesson 3. In fact, node is described as an asynchronous event driven JavaScript runtime on nodejs.org, the official site for node.js.
The previous lessons were not particularly difficult. However, asynchronous programming is challenging. This is probably the most important lesson in learnyou node. Everything moving forward builds on this lesson. Understanding asynchronous processing is critical to programming with node.
The lesson hint suggest reading up on callbacks in Max Ogden’s Art of Node article on Git. However, I recommend taking the time to read the who thing. In fact, the solution that I provide for this lesson is built upon Ogden’s getNumber example.
Callbacks are part of the JavaScript language. A callback is a type of function that executes at a later time. Imagine you are a contractor building a house. You need to have the cabinets installed, the walls painted, and molding installed along the walls. The walls will be white, and the cabinets and trim will be oak. You find a painter, a cabinet installer, and a finisher to do the work, but you obviously can’t have all three of them do their jobs at the same time. On top of that, the order that the tasks are completed matter. If the cabinets and molding are installed first, the painter might accidentally get white paint on the oak woodwork. On the other hand, if the cabinet installer and finisher try to do their jobs right after the painter leaves, they might ruin the paint. You, as the contractor tell the painter to call the cabinet installer and finisher back after he is done has done and the paint has dried. That is a callback. One function tells another function it will call the other function back once it does its job.
An alternative to callbacks are promises. Promises are outside of the scope of this example. However, if you would like to learn more about them, check out What Pinky and the Brain can teach us about Promises and Currys.
In order to print the number of lines in our file, we need to use the console.log() method inside of a callback function. If we place it outside of a callback function, the console.log() and fs.readFile() methods will run at the same time. However, console.log() will complete significantly faster because it does not need to talk to the hard drive. As a result, console.log() will print ‘undefined’ to the console instead of the number of lines in the file.
Official Solution
var fs = require('fs') var file = process.argv[2] fs.readFile(file, function (err, contents) { // fs.readFile(file, 'utf8', callback) can also be used var lines = contents.toString().split('\n').length - 1 console.log(lines) })
As you can see in the official solution above, console.log is placed within a callback function. Let’s break down the structure of the official solution. You can see that ‘fs’ is required just like the previous exercise. The third index of the process object’s argv property is passed to a variable called file. The fs.readFile() method receives two arguments, file, and a callback function() {}. The callback function also has two arguments, err and contents. Inside of the callback function, contents are passed to the variable lines, and converted to a string with the toString() method. Then the string is split at each new line with the split() method. Next we get the length of the split method minus one. Finally, console.log() prints the contents of lines to the console. Note how the callback is encapsulated in the fs.readFile() method.
method(callback(){})
The official solution shows one of the most common methods of employing callbacks that I have observed in the nodesphere. If you wanted to do more than one thing with the results of your function, you could keep adding functions inside your function. However, you could easily find yourself in a place known as callback hell. A deeply nested set of functions that are difficult to read and understand. Imagine callbacks nested with callbacks, nested within callbacks to the point where the code starts to look like an ascii image of the rocky mountains. To avoid this, you can break your functions into multiple functions.
My Solution
var fs = require('fs') var lines = undefined function getLines(callback) { fs.readFile(process.argv[2], 'utf8', function doneReading(err, fileContents) { lines = fileContents.split('\n').length - 1 callback() }) } function logLines() { console.log(lines) } function magicNumber() { var number = +process.argv[3] if (lines >= number){ console.log('This file has '+number+ ' or more lines') } else { console.log('This file does not have '+number+' or more lines') } } getLines(logLines) getLines(magicNumber)
In my solution, I am getting the number of lines in my file with the getLines() function. The getLines() function can receive a callback as an argument. Below, I am declaring two more functions called logLines() and magicNumber(). The logLines() function prints the number of lines in my file to the console, and the magicNumber() function allows me to pass a magic number as the fourth argument in the command line. If my file has as many lines or more than my magic number, it tells me me that it has more as many or more lines. If not, the function says my file does not have as many or more lines.
Below my declared functions, I am passing logLines() and magicNumber() to the getLines() function. When getLines() is done reading a file, it calls both of these functions so that they can do their job with the result of getLines().