EventEmitter is a very important class in Node.js. It provides a channel for events to be dispatched and listeners to be notified. Many objects you’ll encounter in Node.js inherit from EventEmitter, like the Streams we saw in the last section.
Now let’s modify our previous program to capture the child process’s output by listening for events on the stream. Open an editor to the watcher-spawn.js file from the previous section, then find the call to fs.watch(). Replace it with this:
| | fs.watch(filename, () => { |
| | const ls = spawn('ls', ['-l', '-h', filename]); |
| | let output = ''; |
| | |
| | ls.stdout.on('data', chunk => output += chunk); |
| | |
| | ls.on('close', () => { |
| | const parts = output.split(/\s+/); |
| | console.log([parts[0], parts[4], parts[8]]); |
| | }); |
| | }); |
Save this updated file as watcher-spawn-parse.js. Run it as usual, then touch the target file in a separate terminal. You should see output something like this:
| | $ node watcher-spawn-parse.js target.txt |
| | Now watching target.txt for changes... |
| | [ '-rw-rw-r--', '0', 'target.txt' ] |
The new callback starts out the same as before, creating a child process and assigning it to a variable called ls. It also creates an output variable, which will buffer the output coming from the child process.
Notice the output variable declared with the keyword let. Like const, let declares a variable, but one that could be assigned a value more than once. Generally speaking, you should use const to declare your variables unless you know that the value should be able to change at runtime.
Next we add event listeners. An event listener is a callback function that is invoked when an event of a specified type is dispatched. Since the Stream class inherits from EventEmitter, we can listen for events from the child process’s standard output stream:
| | ls.stdout.on('data', chunk => output += chunk); |
A lot is going on in this single line of code, so let’s break it down.
Notice that the arrow function takes a parameter called chunk. When an arrow function takes exactly one parameter, like this one, you can omit the parentheses around the param.
The on() method adds a listener for the specified event type. We listen for data events because we’re interested in data coming out of the stream.
Events can send along extra information, which arrives in the form of parameters to the callbacks. data events in particular pass along a Buffer object. Each time we get a chunk of data, we append it to our output.
A Buffer is Node.js’s way of representing binary data.[19] It points to a blob of memory allocated by Node.js’s native core, outside of the JavaScript engine. Buffers can’t be resized and they require encoding and decoding to convert to and from JavaScript strings.
Any time you add a non-string to a string in JavaScript (like we’re doing here with chunk), the runtime will implicitly call the object’s toString() method. For a Buffer, this means copying the content into Node.js’s heap using the default encoding (UTF-8).
Shuttling data in this way can be a slow operation, relatively speaking. If you can, it’s often better to work with Buffers directly, but strings are more convenient. For this tiny amount of data the impact of conversion is small, but it’s something to keep in mind as you work more with Buffers.
Like Stream, the ChildProcess class extends EventEmitter, so we can add listeners to it, as well.
| | ls.on('close', () => { |
| | const parts = output.split(/\s+/); |
| | console.log([parts[0], parts[4], parts[8]]); |
| | }); |
After a child process has exited and all its streams have been flushed, it emits a close event. When the callback printed here is invoked, we parse the output data by splitting on sequences of one or more whitespace characters (using the regular expression /\s+/). Finally, we use console.log to report on the first, fifth, and ninth fields (indexes 0, 4, and 8), which correspond to the permissions, size, and filename, respectively.
We’ve seen a lot of Node.js’s features in this small problem space of file-watching. You now know how to use key Node.js classes, including EventEmitter, Stream, ChildProcess, and Buffer. You also have firsthand experience writing asynchronous callback functions and coding for the event loop.
Let’s expand on these concepts in the next phase of our filesystem journey: reading and writing files.