24 Behavioral Through Observer Iterator Patterns Look at Js Async Callbacks

24 Behavioral: Understanding JS Asynchronous Callbacks through Observer and Iterator Patterns #

Hello, I am Ishikawa.

After discussing creational and structural design patterns, in this lesson, we will learn about behavioral design patterns. We previously mentioned that front-end programming is “event-driven,” meaning that it heavily relies on user and application interactions. Typically, our programs react accordingly based on user screen clicks or page scrolling actions. This can be observed in our earlier examples of React and Vue, where the concept of “reactive programming” strongly influences front-end application design.

Today, we will focus on the observer pattern within the behavioral design patterns. It is a reflection of the event-driven approach at the design level. Through this lesson, you will gain a better understanding of the event-driven and asynchronous nature of JS development.

24 Behavioral Through Observer Iterator Patterns Look at JS Async Callbacks #

When it comes to event-driven programming, two main objects cannot be ignored: the change observable and the observer. The observable object changes due to events, and the observer reacts to these changes.

Image

We can understand this pattern through a simple example illustrated above. Let’s assume we have two observers, observer1 and observer2, with initial values of 11 and 21 respectively. The observable object triggers an event that increases the value by 1. As a result, the values of observer1 and observer2 are updated to 12 and 22, respectively.

Now let’s take a look at the implementation below. Typically, the implementation of an observable object follows a template pattern, while the observer is designed with different logic based on specific reaction requirements.

class Observable {
  constructor() {
    this.observers = [];
  }
  subscribe(func) {
    this.observers.push(func);
  }
  unsubscribe(func) {
    this.observers = this.observers.filter(observer => observer !== func);
  }
  notify(change) {
    this.observers.forEach(observer => { observer.update(change); });
  }
}

class Observer {
  constructor(state) {
    this.state = state;
    this.initState = state;
  }
  update(change) {
    let state = this.state;
    switch (change) {
      case 'increase':
        this.state = ++state;
        break;
      case 'decrease':
        this.state = --state;
        break;
      default:
        this.state = this.initState;
    }
  }
}

// Usage
var observable = new Observable();

var observer1 = new Observer(11);
var observer2 = new Observer(21);

observable.subscribe(observer1);
observable.subscribe(observer2);

observable.notify('increase');

console.log(observer1.state); // 12
console.log(observer2.state); // 22

In this event-driven example, we utilize the Observer pattern. The Observer pattern, as a behavioral pattern, is one of the most widely discussed and prevalent patterns, representing the manifestation of event-driven design at a higher level. The most common example of event-driven programming is UI events. For example, sometimes we need our program to react based on touch or swipe events. In addition to UI events, there are two other frequently used scenarios for event-driven programming, namely network and backend events.

Image

First, let’s discuss network events. This is a highly used scenario for the Observer pattern, primarily because most applications nowadays utilize XHR-like patterns to dynamically load and display content on the frontend. Typically, we wait for client requests to reach the server through the network, receive their respective responses, and then perform any required operations. To achieve this, we subscribe to different states using the Observer pattern and react accordingly based on these states.

Another scenario is backend events, such as in Node.js. The Observer pattern is also crucial in this context, so much so that it is supported by the built-in EventEmitter functionality. For example, the “fs” module in Node.js provides an API for handling files and directories. We can treat a file as an object, and when it is opened, read, or closed, it has different state events. During this process, if we want to notify and handle these different events, we can utilize the Observer pattern.

Event-driven and Asynchronous #

We previously mentioned that the observer pattern is often related to event-driven programming. So, what is the relationship between event-driven programming and asynchronous programming?

This relationship is not difficult to understand. Some computer programs, such as scientific simulations and machine learning models, are computationally intensive. They run continuously without pausing until they produce a result. This is called synchronous programming. However, most real-world computer programs are asynchronous, meaning that they often have to pause their execution while waiting for data to arrive or for certain events to occur. JavaScript programs in web browsers are typically event-driven, which means they wait for user clicks before performing any actual operations. The same applies to network events or backend events, where the program must wait for a certain state or action to start running.

So, the relationship between the observer pattern and asynchronous programming is this: events are generated asynchronously, and we need to react to these asynchronously generated events by observing them.

Image

JavaScript provides a set of features that support the asynchronous observer pattern, namely callbacks, promises/then, generators/next, and async/await. Now, let’s take a closer look at each of these patterns.

Callback Pattern #

In JavaScript, the callback pattern is an operation where we pass the result of one function as an argument to another function when it finishes. In functional programming, this way of passing results is called the continuation passing style (CPS). It means that the function does not directly return the result, but passes it through a callback. As a general concept, CPS does not necessarily represent asynchronous operations; it can also be used for synchronous operations. Let’s look at synchronous and asynchronous CPS separately.

Synchronous CPS #

First, let’s look at synchronous CPS. The following addition function should be easy to understand. It takes the values of a and b, adds them together, and returns the result. This style is called direct style.

function add(a, b) {
  return a + b;
}

So, how would synchronous CPS look like? In this example, syncCPS does not directly return the result; instead, it returns the result of a + b through a callback.

function syncCPS(a, b, callback) {
  callback(a + b);
}

console.log('Before sync');
syncCPS(1, 2, result => console.log(`Result: ${result}`));
console.log('After sync');

// Output:
// Before sync
// Result: 3
// After sync

Asynchronous CPS #

Now let’s look at asynchronous CPS. The most classic example is setTimeout. In the example code below, you can see that asynchronous CPS also does not directly return the result; instead, it returns the result of a + b through a callback. However, in this case, we use setTimeout to delay the result for 0.1 seconds. As you can see, when it reaches setTimeout, it does not wait for the result but returns to asyncCPS and continues with the next task console.log('After async').

function asyncCPS(a, b, callback) {
  setTimeout(() => callback(a + b), 100);
}

console.log('Before async');
asyncCPS(1, 2, result => console.log(`Result: ${result}`));
console.log('After async');

// Output:
// Before async
// After async
// Result: 3

In the above example, the function calls and the order of control flow can be represented by the following diagram:

      ┌──────────────────┐
      │    syncCPS(1, 2)  │
      └──────────┬───────┘
                 │
                 │
                 ▼
      ┌──────────────────┐
      │ callback(3)     │
      └──────────┬───────┘
                 │
                 │
                 ▼
┌───────────────────────────────────────┐
│              console.log('Result:', result)    │
└───────────────────────────────────────┘

You might wonder if synchronous CPS has any meaning. The answer is no. While we gave an example above to demonstrate that synchronous CPS is possible, it is not necessary to use CPS if the function is synchronous. Using direct style instead of synchronous CPS to implement synchronous interfaces is always a more reasonable practice.

Callback Hell #

Before ES6, we could almost only do asynchronous callbacks using callbacks. For example, in the code example shown below, we want to fetch the machine information for a Pokémon using a publicly available library based on XMLHttpRequest.

(function () {
  var API_BASE_URL = 'https://pokeapi.co/api/v2';
  var pokemonInfo = null;
  var moveInfo = null;
  var machineInfo = null;
  
  var pokemonXHR = new XMLHttpRequest();
  pokemonXHR.open('GET', `${API_BASE_URL}/pokemon/1`);
  pokemonXHR.send();
  
  pokemonXHR.onload = function () {
    pokemonInfo = this.response
    var moveXHR = new XMLHttpRequest();
    moveXHR.open('GET', pokemonInfo.moves[0].move.url);
    moveXHR.send();
  
    moveXHR.onload = function () {
      moveInfo = this.response;
      var machineXHR = new XMLHttpRequest();
      machineXHR.open('GET', moveInfo.machines[0].machine.url);
      machineXHR.send();
      
      machineXHR.onload = function () { }
    }
  }
})();

As you can see, in this example, we have to establish a new HTTP request every time we want to fetch the next level of API data. Furthermore, these callbacks are nested inside each other. If it is a large-scale project, having this many levels of nesting is not a good code structure. This problem of multiple levels of nested asynchronous calls is known as “callback hell.” It is a challenge faced when using callbacks for asynchronous operations. So, how do we solve this problem? Let’s see how the appearance of promises and async solves this problem.

Asynchronous patterns in ES6+ #

Since ES6, JavaScript has gradually introduced many hardcore tools to help handle asynchronous events. From the beginning with Promises, to Generators and Iterators, and later on with async/await. The problem of callback hell has been gradually solved, allowing asynchronous handling to see the light of day again. Below, we will start with Promises and see how this problem has been solved step by step.

Promises #

Since ES6, JavaScript has introduced a series of new built-in tools to help handle asynchronous events. The first of these is the promise and then. We can use the chaining method with then to perform the next operation after each fetch. Let’s look at this piece of code, which reduces a lot of XMLHttpRequest code, but still remains within layers of calls. So this code is not elegant either.

(function () {
  var API_BASE_URL = 'https://pokeapi.co/api/v2';
  var pokemonInfo = null;
  var moveInfo = null;
  var machineInfo = null;
  
  var showResults = () => {
    console.log('Pokemon', pokemonInfo);
    console.log('Move', moveInfo);
    console.log('Machine', machineInfo);
  };

  fetch(`${API_BASE_URL}/pokemon/1`)
  .then((response) => {
    pokemonInfo = response;
    fetch(pokemonInfo.moves[0].move.url)
  })
  .then((response) => {
	moveInfo = response;
	fetch(moveInfo.machines[0].machine.url)
	})
  .then((response) => {
	machineInfo = response;
	showResults();
  })
})();

Generators and Iterators #

So how can we execute asynchronous calls in a synchronous way? In the ES6 version, along with the introduction of Promises and then, the concepts of Generators and Iterators were also introduced. Generators allow a function to pause after executing one line of code with yield, then execute external code, and return to execute the next statement within the function when next is called in the external code. Isn’t it amazing! The next in this example is actually a manifestation of the iterator pattern in the behavioral design pattern.

function* getResult() {
    var pokemonInfo = yield fetch(`${API_BASE_URL}/pokemon/1`);
    var moveInfo = yield fetch(pokemonInfo.moves[0].move.url);
    var machineInfo = yield fetch(moveInfo.machines[0].machine.url);
}

var result = showResults();

result.next().value.then((response) => {
    return result.next(response).value
}).then((response) => {
    return result.next(response).value
}).then((response) => {
    return result.next(response).value

async/await #

But using next also has its problems, as the callback chain can also become very long. Realizing the problems with promise/then, JavaScript introduced the concept of async/await in the ES8 version. This way, each asynchronous operation like pokemonInfo, moveInfo, etc. can be independently awaited, while still maintaining a concise syntax similar to synchronous code. Let’s take a look below:

async function showResults() {
  try {
    var pokemonInfo = await fetch(`${API_BASE_URL}/pokemon/1`)
    console.log(pokemonInfo)
  	var moveInfo = await fetch(pokemonInfo.moves[0].move.url)
    console.log(moveInfo)
  	var machineInfo = await fetch(moveInfo.machines[0].machine.url)
    console.log(machineInfo)
  } catch (err) {
    console.error(err)
  }
}
showResults();

Summary #

Today, I took you through the design patterns of asynchronous programming, such as the Observer and Iterator patterns. It can be said that event-driven programming, reactive programming, and asynchronous programming encompass a significant portion of the core concepts of JavaScript design patterns. Although this article is not long, it is indeed essential content for understanding and applying the core of JavaScript.

Of course, asynchronous programming is a vast topic, and we cannot cover it all in one go today. In the future, we will continue to explore parallel and serial development in asynchronous programming, and when discussing multithreading, we will have a deeper understanding of the implementation logic of asynchronous programming.

Thought-provoking question #

Finally, let me leave you with a thought-provoking question. We have previously discussed that CPS is equivalent to callbacks. Can we say that callbacks are also CPS?

Please feel free to share your answers, exchange learning experiences, or ask questions in the comment section. If you find this helpful, you are also welcome to share today’s content with more friends. See you in the next issue!