This content originally appeared on Telerik Blogs and was authored by Ashnita Bali
Functions, promises, iterables and observables are the producers in JavaScript. Each can produce a value/sequence of values and send it to consumers.
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.
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 sequencedone
istrue
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 tofulfilled
- 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.
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:
- 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);
- 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
- Async Programming in ES7 | JSConf US 2015 by Javar Husain.
- Introduction to Processes and Threads for Web UI Developers (coming soon)
This content originally appeared on Telerik Blogs and was authored by Ashnita Bali
Ashnita Bali | Sciencx (2021-11-29T09:39:01+00:00) Angular Basics: Comparing Data Producers in JavaScript—Functions, Promises, Iterables and Observables. Retrieved from https://www.scien.cx/2021/11/29/angular-basics-comparing-data-producers-in-javascript-functions-promises-iterables-and-observables/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.