Event Emitters as Generators in JavaScript

Async generators allow using a series of async events as if they were iterable. It’s probably the first significant step to take to learn reactive programming.

Problems with events

When you are using event emitter (let’s say it’s eventemitter3) you may produce code similar to this one to process a series of events. Let’s also make a few assumptions for the sake of example:

  • You wwait for all images on the website to be loaded.
  • There will be no additional images added to the running website later, so after they all load, it’s done.
import EventEmitter from "eventemitter3";

function handleImageLoaded(evt) {
  console.log(evt.data);
}

const events = new EventEmitter();

events.on("imageloaded", handleImageLoaded);

The above code may be fine but has a few problems. When you subscribe to the imageloaded event, you can’t determine when will the last event fire or do something after a limited series of events start coming in.

To remedy that you can for example:

Trigger an event with data that holds info about the current number of loaded images and the total number of images to be loaded like this:

function handleImageLoaded(evt) {
  if (evt.data.loadedImagesCount === evt.data.totalImagesCount) {
    handleAllImagesLoaded();
  }
}

Or maybe trigger another event to indicate that everything is loaded and unsubscribe to the first event:

import EventEmitter from "eventemitter3";

const events = new EventEmitter();

function handleImageLoaded(evt) {
  console.log(evt.data);
}

function handleAllImagesLoaded(evt) {
  events.off("imageloaded", handleImageLoaded);
}

events.on("imageloaded", handleImageLoaded);
events.on("allimagesloaded", handleAllImagesLoaded);

As you can see code like that gets really complicated quickly and it’s easy to make a bug (by subscribing/unsubscribing to events in an incorrect order) or just not unsubscribing to some events and having memory leaks this way, etc.

Async generators to the resque

If the code above was rewritten as an async generator (stream) instead of events it would have a lot less ambiguity (there will be less possible ways to do the same things) and it will have less complexity in general.

For example, if you have a generator like that:

async function* getLoadedImages() {
  // this code is unoptimized, because it's not the scope of this article,
  // it's here just to show the general idea about generators
  const loaded = [];

  while (loaded.length !== document.images.length) {
    for (let image of document.images) {
      if (image.complete && !loaded.includes(image)) {
        loaded.push(image);
        yield image;
      }
    }
  }
}

You can use it in a much more linear (procedural like) way than events. The only catch is, a function that uses async generators must be async also, but I don’t think it can be called a “drawback”:

async function main() {
  for await (let loadedImage of getLoadedImages()) {
    console.log("image loaded", loadedImage);
  }

  console.log("all images loaded");
}

main();

It’s also possible to chain async function, because it’s actually syntax sugar around Promises.

main().then(function () {
  console.log("all images are loaded");
});

Summary

Generators can solve a lot of problems, but they are not a silver bullet, so despite all their benefits and good parts, please do not try to apply them wherever possible just because someone wrote a blog post about them. :)

I like them and recommend using generators and reactive programming, but just make up your own mind.

Thank you for reading, I hope this article helps you.