Telerik blogs
AngularT2 Dark_1200x303

Functions, promises, iterables and observables are the producers in JavaScript. Each can produce a value/sequence of values and send it to consumers.

kittens in a basket

Photo credit: Jari Hytönen on Unsplash.

Producers and Consumers of Data

Our applications often contain code that produces data and code that uses that data. Code responsible for producing data is called the producer or the data source, while code that consumes the data is called the consumer.

A producer encapsulates the code for producing data and provides the means to communicate with the consumer. A producer may produce any kind of data. It may get the data by fetching it from an API, listening to DOM events, performing a calculation based on input values or even store hard-coded data.

The diagram below illustrates that producers vary in when and how they produce data as well as how they send data to the consumer.

Producer (produces data) with an arrow toward Consumer (consumes data). The arrow contains: push/pull, lazy/eager, single value/sequence of values, synchronous/asynchronous, unicast/multicast

Icons made by Freepik from www.flaticon.com.

A producer may:

  • have a pull or push system
  • have lazy or eager execution
  • return a single value or emit a sequence of values
  • carry out a synchronous or an asynchronous operation to produce data
  • unicast or multicast data to consumers

Any guesses to what producers are available in JavaScript?

Producers in JavaScript

Functions, promises, iterables and observables are the producers in JavaScript. Each can produce a value, or in some cases a sequence of values, and send it to the consumers.

Functions and promises both return a single value. However, functions are synchronous and lazy, whereas promises are asynchronous and eager.

Iterables and observables allow us to work with sequences of data (also known as streams of data). However, iterables are synchronous and lazy, while observables can produce data synchronously or asynchronously.

Functions, promises and iterables are built in to JavaScript. Whereas observables are not part of JavaScript yet and are implemented by libraries such as RxJS.

Let us have a closer look at each in turn.

Functions

Functions produce a single value. A function takes input, does some operation on the input and returns a single value as output. If the function body does not have a return statement to return a value, it implicitly returns undefined.

function sumNaturalNumbers(num) {
  if (num <= 1) {
    return num;
  }
  return sumNaturalNumbers(num - 1) + num;
}

Functions are executed lazily. We won’t get any data from our function declaration above because functions are inert. The function declaration only defines the parameters and says what to do in the body. The code within the function body isn’t executed until we call the function and pass in any arguments. The function will only return a value when we ask it to—that is why we call it lazy. Functions are executed lazily or on demand.

The caller (consumer) is in control of when it receives data from a function. They pull the data out of the function.

Our sumNaturalNumbers() function is not executed until we call it:

sumNaturalNumbers(10);

Functions are synchronous. When we call a function, the JavaScript engine creates a function execution context containing the function’s arguments and local variables and adds it to the JavaScript callstack.

The JavaScript engine executes each line of code in the function body until the function returns. Then the JavaScript engine removes the function’s execution context from the JavaScript callstack.

Function calls (except asynchronous callbacks) run directly on the main thread of the browser’s renderer process. The main thread of the renderer process is responsible for running our web application’s JavaScript. The synchronous code in our application runs directly on the main thread—it is added to the top of the callstack (without waiting for the callstack to be empty first).

Whereas asynchronous callbacks must first wait in a queue before they can run on the main thread. We use Web APIs to perform asynchronous tasks in our applications. For example, to fetch data from the network or run CPU-intensive operations on worker threads. We process the results of these tasks in our application through callback functions and event handlers.

Once the asynchronous task is complete, the thread performing the asynchronous task queues the callback to a task queue or microtask queue. The event loop executes the queued callbacks on the main thread when the JavaScript callstack is empty.

Great, let us look at iterables next. 🌼🌸🌺

Iterables

Iterables were introduced to JavaScript in ES2015. An object is iterable if it has a Symbol.iterator method that returns an iterator object.

The iterator object has a method called next() that lets us iterate over the values in the iterable.

Calling an iterator.next() returns an object with two properties:

  • value is the next value in the iteration sequence
  • done is true if there are no more values left in the sequence

Let us create an iterator to iterate over an iterable.

Generator functions make it easy to create an iterable and its iterator. 🦋 The function keyword followed by an asterisk (function*) defines a generator function.

We can think of the yield keyword as intermediate returns. Using yield we can return multiple values before hitting the final return statement.

function* generateVowelsIterator() {  
    yield 'a';
    yield 'e';
    yield 'i';
    yield 'o';
    yield 'u';  
    return true;
}

To consume data from the generator function, we request an iterator—calling a generator function returns an iterator:

const vowelsIterator = generateVowelsIterator();  

We can now call next() on the iterator. This asks the generator function to evaluate the first yield expression and return the value. Each time we call iterator.next() the generator function evaluates the next yield statement and returns the value, till the function returns the final value and sets done to true.

vowelsIterator.next(); // {value: "a", done: false}  
vowelsIterator.next(); // {value: "e", done: false}  
vowelsIterator.next(); // {value: "i", done: false}  
vowelsIterator.next(); // {value: "o", done: false}  
vowelsIterator.next(); // {value: "u", done: false}  
vowelsIterator.next(); // {value: undefined, done: true}

Like functions, generator functions can accept parameters, so instead of hard-coding the yielded values, we can make a more generic iterator:

function* generateWordIterator(word) {  
  let count = 0;  
  for (let i = 0; i < word.length; i++) {  
    count++;  
    yield i;  
  }  
  return count;  
}

We don’t actually need to create custom iterators to iterate over values in a string. Very conveniently for us, in ES6 collections became iterable. Thus, the string, array, map and set types are built-in iterables in JavaScript. Each of these types have a Symbol.iterator method in their prototype chain that returns their iterator.

Let us redo our vowels example then. We can store the vowels in a string and iterate over it using the for...of statement:

const vowels = 'aeiou';

for (let vowel of vowels) {  
  console.log(vowel);  
}

We often use the for...of statement, the spread operator [...'abc'] and destructuring assignments [a,b,c]=['a', 'b', 'c'] to iterate over values. Behind the scenes, they ask the iterable for an iterator object to iterate over their values.

Now that we’ve looked at examples of iterators, how do they compare with functions?

Just like functions, iterators are lazy and synchronous. Unlike functions, an iterable can return multiple values over time through its iterator. We can keep calling iterator.next() to get the next value in the sequence until the sequence is consumed.

Let us look at promises next. 🎁

Promises

A Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value (or error).

const myPromise = new Promise((resolve, reject) => {
    // setTimeout is an asynchronous operation
    setTimeout(() => {  
      resolve('some value');  
  }, 1000);  
})

We pass success handlers to a promise by calling its then() method. Similarly, we pass an error handler to a promise by calling its catch() method.

(We could pass in error handlers as the second parameter to the then() method as well—however, it is more common to leave error handling to the catch() method.)

myPromise  
  .then(successHandlerA)  
  .then(successHandlerB)  
  .catch(errorHandler);

A promise object has two properties:

  • status—as the name suggests, status stores the status of the promise (pending, fulfilled or rejected)
  • value—the value returned from the asynchronous operation

While the asynchronous operation is still in progress, the promise is pending and the value is undefined.

If the operation completes successfully then the promise object:

  • updates its state property to fulfilled
  • sets its value to the value returned by the asynchronous operation
  • adds the success callbacks together with the promised value to the microtask queue

On the other hand, if the asynchronous operation has an error the promise object:

  • updates its state to rejected
  • sets its value to the error information
  • adds the error callback to the microtask queue with the error information

In short, a promise either resolves to a value when the asynchronous operation is completed successfully, or it resolves with a reason for an error if the operation fails.

Promises are always asynchronous as they add the success or error callback to the microtask queue. The event loop executes the queued callbacks when the JavaScript callstack is empty.

Unlike functions and iterables, promises are not lazy, but eager. A promise in JavaScript represents an asynchronous action that has already been started. For example, calling fetch() starts the asynchronous operation of requesting for the specified resource from the network and returns the promise that represents that operation.

const pikachuPromise = 
fetch('https://pokeapi.co/api/v2/pokemon/pikachu');

pikachuPromise
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

Promises are multicast. The callbacks will be invoked even if they were added after the success or failure of the asynchronous operation that the promise represents.

Let us look at observables 🕶 next and see how they compare with promises, iterables and functions.

Observables

An observable represents a sequence of values that can be observed. — TC39

Observables are lazy Push collections of multiple values. — RxJS

Observables fill the missing spot for a producer in JavaScript that can send a sequence of values asynchronously. This is illustrated in the following table:

  Single  Multiple
Pull Function Iterator
Push Promise Observable

Observables provide a unified way to work with different kinds of data. They can produce:

  • A single value (like functions and promises) or multiple values (like iterables)
  • Synchronously (like functions and iterables) or asynchronously (like promises)
  • Lazily (cold observable) or eagerly (hot observable)
  • Unicast to a single consumer (cold observable) or multicast to multiple consumers (hot observable)

Unlike promises and iteration protocols, observables are not part of JavaScript yet. However, there is a TC39 proposal to add an observable type to JavaScript. We can use libraries that implement the observable type, most popular of which is RxJS (with 24,895,323 npm weekly downloads at the time of writing).

The trick to understanding observables lies in seeing how an observable instance is created.

We pass a subscriber function to the observable constructor.

The subscriber function takes an observer as its input parameter. An observer is an object with properties that contain the next, error and complete callbacks.

We define the logic for producing data in the subscriber function, and send data to the observer by calling the next() callback. Likewise, we notify the observer of an error by calling the error() callback and of completion by calling the complete() callback.

import { Observable } from 'rxjs';

const myObservable$ = new Observable(subscriber);

function subscriber(observer) {  
  // 1. produce data

  // 2. emit data
  // 3. notify if error
  // 4. notify if/when complete
  
  // 5. return a function which will be executed when unsusbcribing from the observable
  return () => {
    // teardown logic
  };
}

To consume data from the observable, we need to first subscribe to the observable instance by calling the subscribe method and passing in an observer. Subscribing to the observable instance executes the subscriber function, which produces data and and calls the appropriate callbacks when it has data, an error occurs or it is complete.

myObservable$.subscribe({
  next: (data) => // do stuff with data, 
  error: (error) => // handle error, 
  complete: () => // handle completion
});

However, we don’t usually need to define the logic for creating an observable instance ourselves. The RxJS library provides observable creation functions for common used cases, such as of, fromEvent, interval, concat and many more.

For a more in-depth study of the observable type, please (please, please) read my article Introduction to Observables (RxJS). 🙏🏽

Pull vs. Push Systems

Pull

In a pull system, the consumer pulls the data from the producer. The consumer is in control and it decides when to get the data—it pulls the data from the producer when it wants.

The pull system is suitable for data produced synchronously, allowing the consumer to get data whenever it asks for it without having to wait and without blocking.

The main thread of the renderer process is responsible for:

  • rendering the web page
  • responding to user inputs
  • as well as running the application’s JavaScript

The main thread can only do one task at a time. Therefore, if a function takes too long to return, while it is running, the function blocks the main thread and prevents it from rendering the page and responding to user inputs.

Examples

Two of the producers in JavaScript have a pull system:

  1. Functions

As shown in the code below, we pull the value out of a function by calling the function.

function sum(a, b) {  
  return a + b;  
}
const cost = sum(1, 2);
  1. Iterables

In the code below, we pull the values out of the array (which is an iterable) using a destructuring assignment. The destructuring assignment uses the array’s built-in iterator to traverse through the elements in the colorPalette array and assign the value to the corresponding variables royalblue, etc. specified in the array destructuring.

const colorPalette = ['hsl(216,87%,48%)', 'hsl(216,87%,48%)', 'hsl(42,99%,52%)', 'hsl(7,66%,49%)'];

const [royalblue, seagreen, orange, firebrick] = colorPalette;

Push

In a push system, the producer pushes data to the consumer when the data is available.

The consumer lets the producer know that they’re interested in receiving data. However, the consumer does not know when the data will arrive. For example, if the consumer asked the producer for data that needs to be fetched from the network, factors such as the network connectivity affect the time it takes for the producer to receive data.

The consumer doesn’t want to block the renderer thread while it waits for the data from the producer. Neither does it want to keep checking with the producer to see if the data is available yet. What can the consumer do instead? It can send the producer a callback!

Callback Functions

The consumer can define a function that accepts the data as input and implements the logic to process the data. It can send this function to the producer. Such a function is called a callback. When the producer has the data available, it can call the callback function, passing in the data as an argument.

Additionally, the consumer can send callback functions to handle errors and a callback to be notified that the producer has finished sending all the data (if the producer allows it).

Promises and observables are both examples of a push system. We have already met the callbacks they accept:

 Callback  Promise  Observable
To process data then() next()
To handle error catch() error()
To handle completion - complete()

The push system is really well suited for processing asynchronous data. The consumer does not have to wait for the data, it simply passes its callbacks to the producer who will execute the appropriate callback when it is ready.

Having said that, observables can produce and emit data synchronous as well as asynchronously.

Promises queue the callbacks in a microtask for the event loop to execute. Observable that carry out an asynchronous operation to get data queue the callbacks in a task queue for the event loop to execute.

Although promises and observables are both push systems, they have plenty of distinctions. Promises are always multicast, asynchronous, eager and resolve to a single value. Whereas observables can be unicast or multicast, synchronous or asynchronous, return a single value or multiple values, and are lazy if cold and eager if hot.

Now we’ve seen that observable and promises are both push systems, let us see what observables have in common with iterables next.

Data Streams—The Iterator and Observer Design Patterns

Iterables and observables both deal with streams of data. Instead of returning a single value to the consumer, iterables and observables can send a sequence of values. The sequence could contain zero or more values.

Iterables and observables are based on the iterator and observer behavioral patterns described by the Gang of Four in their popular book, “Design Patterns: Elements of Reusable Object-Oriented Software.”

Iterator Design Pattern

The iterator pattern describes the semantics for a client (consumer) to iterate over a sequence of values (the iterable). The iterator pattern includes semantics for error and completion. It describes a pull relationship between the producer and the consumer.

The iterable and iterator protocols were added to ECMAScript 2015.

The iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container’s elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled. — Wikipedia

Observer Design Pattern

The observer pattern does the same as the iterator but in the opposite direction. It describes a push relationship between the producer and the consumer.

Observables are not part of ECMAScript yet (however, there is a TC39 proposal to add observables to ECMAScript). We can use observables through the RxJS library.

Although the observer pattern described by the Gang of Four does not include the semantics for completion, clever folks in the JavaScript community realized the power of a push-based system that notifies the consumer of completion. I really like the talks by Jafar Husain who explains this beautifully. For example, in this talk Jafar demonstrates how easy it is to create a mouse drag collection using observables because observables can let their subscribers know when they have completed producing data.

The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. — Wikipedia

Summary

The table below presents a sweet and simple summary of what we’ve covered in this article:

 Producer  Characteristics
Function Single value, synchronous, lazy, pull
Promise Single value, asynchronous, eager, pull
Iterable Multiple values, synchronous, lazy, push
Observable Multiple values, synchronous or asynchronous, lazy or eager, push

Further Resources


ashnita-bali
About the Author

Ashnita Bali

Ashnita is a frontend web developer who loves JavaScript and Angular. She is an organizer at GDGReading, a WomenTechmakers Ambassador and a mentor at freeCodeCampReading. Ashnita is passionate about learning and thinks that writing and sharing ideas are great ways of learning. Besides coding, she loves the outdoors and nature.

Related Posts

Comments

Comments are disabled in preview mode.