This is my third article for beginners in participation

Why do you need Rxjs when you have Promise?

So how do we choose between Rxjs and Promise?

What is the difference between Promises and Observables? , everything has been introduced.

When writing Angular, you sometimes need to return promises. If we sometimes get promises and need to do other things, we can use the powerful operator features of Rxjs to complete many complex operations. Rxjs and Promise exchange skills, we must master, in order to play their respective advantages.

The difference between Promises and Observables is emphasized in this paper. The goal is to make it easier to understand Observables if you already know Promises(and vice versa). For this reason, I will not discuss RxJS operators in this article, because there is nothing in Promises that is comparable to these operators.

Asynchronous programming in JavaScript

Before we get to them, we have to talk about asynchronous programming in JavaScript.

First, let’s review what Promises and Observables are for: Handling asynchronous execution.

There are different ways to create asynchronous code in JavaScript. The most important of these are:

  • Callbacks
  • Promises
  • Async/Await
  • RxJS Observables

Let’s talk about them briefly.

Callbacks

This is the traditional approach to asynchronous programming. Provide a function as an argument to another function that performs an asynchronous task. When the asynchronous task completes, the executing function calls the callback function. The main drawback of this approach is that when you have multiple asynchronous tasks, you need to define the callback function within the callback function, which we call callback hell.

Promises

Promise was introduced in ES6 (2015), allowing asynchronous code that is more readable than callbacks.

The main difference between Callbacks and Promises is that with Callbacks you can tell the executing function what to do when the asynchronous task completes, whereas with Promises you can return the executing function to you with a special object (a Promise), Then you can tell Promise what to do when the asynchronous task is complete.

const promise = asyncFunc();
promise.then(result= > {
  console.log(result);
});
Copy the code

That is, asyncFunc immediately returns you a Promise and then provides the action to take when the asynchronous task completes (via its.then method).

Async/Await

Async/Await was introduced in ES8 (2017). This technique should actually be listed under Promises, because it’s just syntactic sugar for promises. This grammatical sugar is really worth studying.

Basically, you can declare a function as an asynchronous function using async and use the await keyword inside the function. You can precede an expression that evaluates to a promise with the await keyword. The keyword await suspends execution of the asynchronous function until the Promise returns Resolved. When this happens, the entire await expression evaluates to the result value of the PROMISE and proceeds with the asynchronous function.

In addition, the asynchronous function itself returns a promise that will return Resolved when the execution of the function body is complete.

function asyncTask(i) {
  return new Promise((resolve) = > resolve(i + 1));
}
async function runAsyncTasks() {
  const res1 = await asyncTask(0);
  const res2 = await asyncTask(res1);
  const res3 = await asyncTask(res2);
  return "Everything done";
}
runAsyncTasks().then((result) = > console.log(result));
Copy the code

The asyncTask function implements an asynchronous task that takes an argument and returns a result. This function returns a Promise that will be resolved when the asynchronous task completes. There’s nothing special about this feature, it’s just a generic feature that returns promises.

On the other hand, declare the runAsyncTasks function as async so that you can use the await keyword inside it. This function calls asyncTask three times, each time with arguments that must be the result of previous asyncTask calls (i.e., we created three asynchronous tasks).

The first await keyword causes execution of runAsyncTasks to stop until the promise returned by asyncTask(0) is resolved. Await asyncTask(0) expression then evaluates and parses the result value of the PROMISE and assigns it to RES1. At this point asyncTask(RES1) is called and the second await keyword causes execution of runAsyncTasks to stop again until the promise returned by asyncTask(res1) is resolved. This continues until all statements in the runAsyncTasks body are finally executed.

As mentioned earlier, the async function itself returns a promise, which is resolved using the return value of the function when the internal execution of the function is complete. Thus, in other words, an async function is itself an asynchronous task (it generally manages the execution of other asynchronous tasks). As you can see in the last line, we call the then function on the returned promise to print out the return value of the async function.

If asyncTask adds a log, it prints:

res1
res2
res3
Everything done
Copy the code

If async/await is just syntactic sugar for promises, then we must be able to implement the above example as pure promises:

function asyncTask(i) {
  return new Promise((resolve) = > resolve(i + 1));
}
function runAsyncTasks() {
  return asyncTask(0)
    .then((res1) = > {
      return asyncTask(res1);
    })
    .then((res2) = > {
      return asyncTask(res2);
    })
    .then((res3) = > {
      return "Everything done";
    });
}
runAsyncTasks().then((result) = > console.log(result));
Copy the code

This code is equivalent to the async/await version, and if our then in runAsyncTasks prints log, it will produce the same output as the async/await version.

The only thing that has changed is the runAsyncTasks function. Now it is a regular function (instead of async) that uses then to make promises returned by asyncTask (instead of wait).

I don’t need to tell you that the async/await version is more readable and understandable than the Promise version. In fact, the main innovation in async/await is to allow asynchronous code to be written with promises that “look like” synchronous code.

RxJS Observables

First, RxJS is the JavaScript implementation of the ReactiveX project. The ReactiveX project aims to provide apis for asynchronous programming in different programming languages.

The basic concept of ReactiveX is the Observer pattern of the Gang of Four (ReactiveX even extends the Observer pattern with completion and error notifications). Thus, the core abstraction of all ReactiveX implementations is an Observable. You can read more about the basic concepts of ReactiveX here.

So, now we know what an RxJS is, but what is an Observable? Let’s try to understand it from two dimensions and compare it to other known abstractions. Dimensions are synchronization/asynchrony and single value/multiple value.

For an Observable, we can say the following is true:

  • Send multiple values
  • Asynchronously emits its value (” push “)

Promises: Promises: Promises: Promises: Promises

  • Sending a single value
  • Asynchronously emits its value (” push “)

Finally, let’s take a look at Iterable, an abstraction that exists in many programming languages and can be used to iterate over all elements of set data structures such as arrays. For iteration, the following conditions should be met:

  • Send multiple values
  • Emit its value synchronously (” pull “)

Note: For synchronous/pull and asynchronous/push, synchronous/pull means that the client code requests a value from the abstraction and blocks until the value is returned. Asynchronous/push means that the abstract notification client code is issuing a new value, and the client code processes the notification.

A single value More than one value
synchronous Get 可迭代
asynchronous Promise Observable

Note: Get here only represents regular data access operations (such as regular function calls).

From the table above, we can say that an Observable to an Iterable equals a get Promise. Or a promise is like an asynchronous GET and an Observable is like an asynchronous iterable.

We can also say that the main difference between a promise and an Observable is that a promise emits only one value, while an Observable emits multiple values.

But let’s look at it in more detail. With a simple GET operation (such as a function call), the calling code will request a value and then wait or block until the function returns the value (which the calling code extracts).

With a promise, on the other hand, the calling code also requests a value, but it does not block until that value is returned. It just starts calculating and then continues to execute its own code. When the Promise is finished evaluating the value, it sends the value to the calling code, which then processes it (pushing the value to the calling code).

Now, let’s look at Iterable. In many programming languages, it is possible to create an iterable object from a collection data structure, such as a group of numbers. Iterable usually has a next method that returns the next unread value from the collection. The calling code can then call Next repeatedly to read all the values of the collection. As mentioned above, each next call is basically a synchronous blocking GET operation (the calling code repeatedly extracts the value).

Observables bring Iterable to the asynchronous world. An Observable, like an Iterable, calculates concurrent stream values. Unlike Iterable, however, with an Observable, the calling code does not extract each value synchronously, but an Observable asynchronously pushes each value into the calling code as quickly as possible. To do this, the calling code provides a handler for the Observable, which is then called in RxJS, and which the Observable calls for each value it evaluates.

The values emitted by an Observable can be anything: elements of an array, the result of an HTTP request (if an Observable emits just one value, it doesn’t always have to be multiple), user input events (mouse clicks, etc.). This makes an Observable very flexible. In addition, since an Observable can only emit a single value, it can do everything a Promise can do, but not the other way around.

In addition, a ReactiveX Observable provides a number of so-called Operators. These functions can be applied to Observables to modify emission sets.

For example, we can configure a map operator like map(value => 2 * value), and then apply this operator to an Observable. As a result, each value emitted by the Observable is multiplied by two before being pushed to the calling code.

import { Observable } from 'rxjs';
/ / create
const observable = new Observable(observer= > {
  for (let i = 0; i < 3; i++) { observer.next(i); }});/ / use
observable.subscribe(value= > console.log(value));
Copy the code

With that, we conclude our overview of JavaScript asynchronous programming techniques. We have seen callbacks, the THEN of a promise can be used to get a single value asynchronously, async/await is the syntactic sugar of a promise, and RxJS Observables can be used to get a value for an asynchronous stream.

Promises make Observables

We will compare Promises and Observables and highlight the differences between them.

The installation

Promise is an ES6 standard, and if you want to provide support in browsers that don’t support ES6, you’ll need to introduce polyfills.

Rxjs is not a standard and we need to install it to use it.

You can install RxJS as follows:

npm install --save rxjs@6
Copy the code

You can import the Observable constructor in the code file (all you need for these examples) by following these steps:

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
Copy the code

However, if you use Node.js, you must import as follows (because Node.js does not yet support import statements) :

const { Observable } = require('rxjs');
Copy the code

Note: If you want to run the following code example that includes Observables, you must install and import the RxJS library. We use the online editor directly here

create

Let’s look at how to create promises and how to create Observables. For simplicity, we’ll first ignore errors and consider only “successful” execution of promises and Observables. We’ll cover errors in the next section.

Note that both promises and Observables have two aspects: creation and use. A Promise/Observable is an object that needs to be created by someone first. Once created, it is usually passed to others who use it. Create defines the behavior of a Promise/Observable and the emitted values, and usage defines what to do with those emitted values.

A typical use case is that a Promise/Observable is created by an API function and returned to the API user. Users of the API will then use these Promises/Observables. Therefore, if you use the API, you usually only use promise/ Observable, and if you are the author of the API, you must also create Promise/Observable.

In the following sections, we’ll first look at the creation of promises/Observables and describe their use in subsequent sections.

Promises:

new Promise(executorFunc);
function executorFunc(resolve) {
  // Some code...
  resolve(value);
}
Copy the code

To create a Promise, you call the Promise constructor and pass it as an argument to the so-called Executor function. When you create a promise, the executor function is called and passed as an argument to the special resolve function (you can name this argument as you want, just remember that the first argument to the executor function is the resolve function, and then you must use it as is).

When you call the resolve function in the body of the Executor function, the promise is moved to the completed state and “emitted” (resolved) the value passed to the resolve function as an argument.

This emitted value will then be used as the parameter to the ondepressing function, which you pass as the first parameter of the Promise’s THEN function to the Promise’s use-aspect THEN function, which we will see later.

Observables:

import { Observable } from 'rxjs';

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
}
Copy the code

To create an Observable, call the Observable constructor and pass it to the so-called subscriber function as an argument. The system calls the subscriber function every time a new subscriber subscribes to an Observable. The subscriber function takes an Observable as an argument. The object has a next method that, when called, emits a value passed as an Observable parameter.

Note: After calling next, the subscriber function continues to run and next can be called multiple times. This is an important difference from promises, which execute the program function to terminate after a call to resolve. Promises can emit at most one value, while Observables can emit any number of values.

Create (with error handling)

The above example does not yet show the full capabilities of Promise and Observables. Errors can occur during promise/ Observable execution, and both techniques provide means to handle errors. Let’s extend the above explanation with error handling.

Promises:

new Promise(executorFunc);
function executorFunc(resolve, reject) {
  // Some code...
  resolve(value); / / success
  // Some code...
  reject(error); / / fail
}
Copy the code

The executor function passed to the Promise constructor actually takes the second argument, the reject function. The reject function is used to send errors in Promise execution. When it is called, the execution function is aborted and the Promise moves to the Rejected state.

On the usage side, this causes the onRejected function, which can be passed to the catch method, to be executed.

Observables:

import { Observable } from 'rxjs';

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value); / / success
  // Some code...
  observer.error(error); / / fail
}
Copy the code

The Observable that is passed as an argument to the subscriber function actually has another method: the error method. Call this method to send an error to the Observable subscriber.

Unlike Next, calling the Error method also terminates the subscriber function, which terminates the Observable. This means that at most one error can be called in the lifetime of an Observable.

Next and Error are still not the whole story. The observer object passed to the subscriber function has another method: complete. The usage is as follows:

function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
  // If there is an error...
  observer.error(error);
  // If all successful...
  observer.complete();
}
Copy the code

The Complete method should be called when an Observable successfully “completes.” Done means there is no more work to do, that is, all values have been emitted. Like the Error method, the complete method terminates the subscriber function, which means that the complete method can be called at most once in the life of an Observable.

Calling the Complete method that Observable executes is recommended, but not required.

use

Now that we’ve covered the creation of promises and Observables, let’s look at how they’re used. Using a Promise or Observable means “subscribing” to it, which in turn means calling the Promise or Observable registration handler functions for each emitted value (one of the Promise values, any number of Observable values).

Processing functions are registered through special methods on promise or Observable objects. These methods are:

  • Promise: then
  • Observable: subscribe

In the following sections, we show the basic use of these methods in promise and Observables. Again, we’ll consider the base case of ignoring error handling first, and then add error handling in the next section.

Note: In the following code snippet, we assume that a Promise or Observable already exists. Therefore, to run the code, you must precede it with a promise or Observable creation statement, such as:

  • const promise = new Promise();
  • const observable = new Observable();

Promises:

promise.then(onFulfilled);
function onFulfilled(value) {
  // Do something with value...
}
Copy the code

Given a Promise object, we call that object’s then method and pass it to the onFulfilled function as an argument. The ondepressing function takes a single parameter. This parameter is the result value of the PROMISE, which is passed to the resolve function in the Promise.

Observables:

Subscribe to it as an Observable, which is done through the Subscribe method of an Observable. In fact, there are two equivalent ways to use the Subscription method. Below, we introduce both of them:

The first:

observable.subscribe(nextFunc);
function nextFunc(value) {
  // Do something with value...
}
Copy the code

In this case, we call the Observable subscription method and pass it its next function as an argument. The next function takes a single argument. This parameter is the current emitted value as long as the Observable emits a value.

In other words, whenever the Observable’s internal subscriber function calls the Next method, the value passed to next is used to call your next function (thus passing the value from Observable to your handler).

The second:

observable.subscribe({
  next: nextFunc,
});
function nextFunc(value) {
  // Do something with value...
  console.log(value);
}
Copy the code

The second option may seem a little strange, but it actually shows better what’s going on behind the scenes.

In this case, instead of using the function as an argument, we call SUBSCRIBE using an object. This object has a single property with a key and function value called next. This feature is just a nice next feature we have above.

Everything else remains the same, we just pass the next function inside the object rather than directly as an argument. But why wrap the handler function in an object before passing it to the SUBSCRIBE method?

Objects that can subscribe in this way are actually objects that implement the Observer interface. You may remember that when we created an Observable in the previous section, we defined a subscriber function that takes a parameter called an Observer. We used the following code:

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
}
Copy the code

The observer argument of the subscriber function corresponds directly to the object we passed to subscribe above (in fact, the object passed to subscribe is first converted from type Observer to subscriber and then passed to the subscriber function, And Subscriber implements the Observer interface).

So, in the second option, we’ve created an object that forms the basis for the actual object to be passed to the Observable Subscriber function, whereas in the first option, we’ve only provided functions that will be used as methods on the object.

Which of these two options you use is up to you. Note: If you use the second option, you must force the object property key of the next function to be called. This is specified by the Observer interface that the object needs to implement.

Use (with error handling)

As with creation, we will now extend the use example to include error handling. In this case, error handling means providing a special handler to handle potential errors represented by a Promise or Observable (in addition to a “regular” handler that handles the “regular” values emitted by a Promise or Observable).

For promises and Observables, errors can occur in either case:

  1. The Promise or Observable implementation calls reject or error, respectively (see Create error).
  2. The Promise or Observable implementation throws an error with the throw keyword.

Let’s look at how to handle these types of errors for Promises and Observables:

Promises:

There are actually two ways to handle errors made by promises. The first uses the second argument of the then method, and the second uses the catch method, both of which are described below.

Select one (the second argument to then):

promise.then(onFulfilled, onRejected);
function onFulfilled(value) {
  // Do something with value...
}
function onRejected(error) {
  // Do something with error...
}
Copy the code

Promise’s then method takes a second function argument, the onRejected function. This function is called when the Promise executor function calls reject, or when the Promise executor function throws an error with the throw keyword.

The onRejected function is provided to handle such errors. If you don’t provide it, errors can still occur, but your code can’t handle them.

Option 2 (Catch method):

promise.then(onFulfilled).catch(onRejected);
function onFulfilled(value) {
  // Do something with value...
}
function onRejected(error) {
  // Do something with error...
}
Copy the code

That is, instead of providing the onFulfilled and onRejected functions to the THEN method, we will only provide the onFulfilled method to the THEN and then call the catch method of the promise returned by the THEN. The onRejected function is passed to the catch method. Note that in this case, we call the catch promise(and return then) the same as the original promise.

In fact, the second option using the catch method is more common than the first. It takes advantage of Promise’s important chained approach. A discussion of the Promise chained method is outside the scope of this article, but can be found here.

The important thing to note about chained methods is that then and catch always return a promise, which allows these methods to be called repeatedly in the same statement, as shown above. The returned promise is the same as the previous promise, or a new promise. The latter case allows nested asynchronous tasks to be handled directly without using any form of nesting (which would result in callback hell if used). This, by the way, is one of the main advantages of Promises over callbacks.

Another point worth noting is that catch is actually nothing special. In fact, the catch method is just syntactic sugar for a particular call to the then method. In particular, calling a catch with the onRejected function as the only argument is equivalent to calling a catch with the undefined first argument and onRejected as the second argument.

Therefore, the following two statements are equivalent:

promise.then(onFulfilled).catch(onRejected);
promise.then(onFulfilled).then(undefined, onRejected);
Copy the code

Thus, we can conceptually reduce the chains of then to pure chains of pure THEN, so that they are sometimes easier to understand.

Observables:

As mentioned in the build, there are two ways to call the Subscribe method of an Observable. One takes an object (implementing an Observer) as a parameter, and the other takes a function as a parameter.

We will introduce these two methods:

Option one (function argument):

observable.subscribe(nextFunc, errorFunc);
function nextFunc(value) {
  // Do something with value...
}
function errorFunc(error) {
  // Do something with error...
}
Copy the code

The only difference is that we pass the second function argument to the SUBSCRIBE method, compared to the case with no error handling in use. The second argument is the error function, which is called when the Subscribe function of Observable calls the error method of the Observer argument it passes, or throws an error with a throw.

Option two (object parameters):

observable.subscribe({
  next: nextFunc,
  error: errorFunc,
});
function nextFunc(value) {
  // Do something with value...
}
function errorFunc(error) {
  // Do something with error...
}
Copy the code

The only difference between this and no error handling is that we add an error attribute to the object we pass to the SUBSCRIBE method. The value of this property is an error handler.

There is actually a third function that can be passed to the SUBSCRIBE method :complete(which we mentioned earlier). This function can be passed either as the third argument to subscribe (function argument) or as an Observer of SUBSCRIBE adding the complete attribute (object argument). The value of this property is the complete handler.

In addition, each specification of the three functions is optional. If you do not provide it, nothing will be done on the corresponding event. In summary, this gives you the following method to call subscribe:

  1. If it is a function argument: can pass one, two or three functions.
  2. If object arguments: Optional function properties containing next, Error, and complete.

Create + Use: Example

We’ll apply all of these concepts to some complete examples and implement them in a real-world example using Promise and Observables.

We will display these chestnuts in our online editor

Promises:

/ / create
const promise = new Promise(executorFunc);
function executorFunc(resolve, reject) {
  const value = Math.random();
  if (value <= 1 / 3) {
    resolve(value);
  } else if (value <= 2 / 3) {
    reject('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)'; }}/ / use
promise.then(onFulfilled).catch(onRejected);
function onFulfilled(value) {
  console.log('Got value: ' + value);
}
function onRejected(error) {
  console.log('Caught error: ' + error);
}
Copy the code

This code creates a Promise that generates a random number between 0 and 1. If the number is less than or equal to 1/3, the Promise is resolved with this value (the value “issues”). If the number is greater than 1/3 but less than or equal to 2/3, the Promise is rejected. Finally, if the number is greater than 2/3, an error is thrown using the JavaScript throw keyword.

This program has three possible outputs:

// log1:
/ / Got value: 0.2109261758959049
// log2:
// Caught error: Value <= 2/3 (reject)
// log3:
// Caught error: Value > 2/3 (throw)
Copy the code

When the promise is resolved (using the resolve function), the output log1 occurs. This will cause the onFulfilled processing function to be executed with the parse value.

When a promise is explicitly rejected (using the rejection function), the output log2 occurs. This will result in the onRejected handler being executed.

Finally, the output log3 occurs when an error is thrown during promise execution. As with explicitly rejecting the promise, this results in the onRejected handler being executed.

In the above code, we used a relatively verbose syntax because we used named functions. It is common to use anonymous functions, which makes the code much cleaner. In this regard, we can rewrite the above code equivalently as follows

/ / create
const promise = new Promise((resolve, reject) = > {
  const value = Math.random();
  if (value <= 1 / 3) {
    resolve(value);
  } else if (value <= 2 / 3) {
    reject('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)'; }});/ / use
promise.then(value= > console.log('Got value: ' + value)).catch(error= > console.log('Caught error: ' + error));
Copy the code

Observables:

import { Observable } from 'rxjs';

/ / create
const observable = new Observable(subscriberFunc);
function subscriberFunc(observer) {
  const value = Math.random();
  if (value <= 1 / 3) {
    observer.next(value);
  } else if (value <= 2 / 3) {
    observer.error('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
  observer.complete();
}
/ / use
observable.subscribe(nextFunc, errorFunc, completeFunc);
function nextFunc(value) {
  console.log('Got value: ' + value);
}
function errorFunc(error) {
  console.log('Caught error: ' + error);
}
function completeFunc() {
  console.log('Completed');
}
Copy the code

Promises: Promises are the same. If the random value is less than or equal to 1/3, the Observable emits the value using the next method of the passed Observer. If the value is greater than 1/3, but less than or equal to 2/3, an error is indicated by the Observer error method. Finally, if the value is greater than 2/3, an error with the throw keyword is thrown. At the end of the subscriber function, call the Observer’s complete method.

This program has three possible outputs:

// log1:
/ / Got value: 0.2109261758959049
// Completed
// log2:
// Caught error: Value <= 2/3 (reject)
// log3:
// Caught error: Value > 2/3 (throw)
Copy the code

The output log1 occurs when a regular value is emitted from Observable. It causes the nextFunc handler function to be executed. The completeFunc handler is executed because the Subscribe function of the Observable also calls complete at the end of it.

When Observable calls observer’s error method, log2 is output. This will cause the errorFunc handler to be executed. Note that this also causes the Subscribe function of the Observable to abort. Therefore, the complete method inside the subscriber function is not called, which means that the completeFunc handler function is not executed either. You can see this because output log1 does not have a full output line.

If the Observable subscriber function throws an error using the throw keyword, the output log3 occurs. It has the same effect as calling the error method, executing the errorFunc handler and aborting the Observable subscriber function (without calling the complete method).

We can rewrite this example with a more concise notation:

import { Observable } from 'rxjs';

/ / create
const observable = new Observable(observer= > {
  const value = Math.random();
  if (value <= 1 / 3) {
    observer.next(value);
  } else if (value <= 2 / 3) {
    observer.error('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
  observer.complete();
});
/ / use
observable.subscribe({
  next(value) {
    console.log('Got value: ' + value);
  },
  error(err) {
    console.log('Caught error: ' + err);
  },
  complete() {
    console.log('Completed'); }});Copy the code

Notice that here we use another use of the SUBSCRIBE method, which takes a single object as an argument and a handler function as its attribute. Another approach is to use a subscribe method with three anonymous functions as arguments, but it is often inconvenient and unreadable to have more than one anonymous function in a parameter list. However, the two uses are exactly the same and you can choose whichever you want.

So far, we have compared the creation and use of promises and Observables. Next, we’ll examine a number of other differences between promises and Observables.

Single value versus multiple values

  • Promises only issue a single value. After that, it is in the finished state and can only be used to query the value, not to calculate and issue new values.
  • Observables can emit any number of values.

Promises:

const promise = new Promise(resolve= > {
  resolve(1);
  resolve(2);
  resolve(3);
});
promise.then(result= > console.log(result));
// logs:
/ / 1
Copy the code

Only the first resolve call resolved in the Executor function is executed, resolving the promise with the value 1. After that, the promise moves to the completed state and the resulting value does not change.

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer= > {
  observer.next(1);
  observer.next(2);
  observer.next(3);
});
observable.subscribe(result= > console.log(result));
// logs:
/ / 1
/ / 2
/ / 3
Copy the code

Each call to observer.next in the subscriber function takes effect, issuing a value and executing the handler.

Pre-parsed with lazy

  • Promises are pre-parsed: Once a promise is created, the Executor function is called.
  • An Observable is lazy: the subscriber function is called only when the client subscribes to an Observable.

Promises:

const promise = new Promise(resolve= > {
  console.log('- Executing');
  resolve();
});
console.log('- Subscribing');
promise.then(() = > console.log('- Handling result'));
// logs:
// - Executing
// - Subscribing
// - Handling result
Copy the code

As you can see, the executor function has already been executed before the promise subscription.

If there is no subscription commitment at all, the executor function may even be executed. You can see that if you comment out the last two lines: still output -executing.

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer= > {
  console.log('- Executing');
  observer.next();
});
console.log('- Subscribing');
observable.subscribe(() = > console.log('- Handling result'));
// logs:
// - Subscribing
// - Executing
// - Handling result
Copy the code

As we can see, the subscriber function is executed only after the subscription to Observable is created.

If the last two lines are commented out, there is no output at all because the subscriber function will never execute.

Observables are also called declarative (declare an Observable, but only execute it when it is used) because they are not executed at definition time, but when other code uses them.

unsubscribe

  • Once a promise has been subscribed to using THEN, the handler passed to then will be called anyway. Once promise execution starts, you cannot tell the Promise to cancel the call to the result handler.
  • After subscribing to an Observable with subscribe, you can unsubscribe at any time by calling the unsubscribe method of the subscribe returned object.

Promises:

const promise = new Promise(resolve= > {
  setTimeout(() = > {
    console.log('Async task done');
    resolve();
  }, 2000);
});
// Handler can no longer be prevented from being executed.
promise.then(() = > console.log('Handler'));
// logs:
// Async task done
// Handler
Copy the code

Once we call THEN, there is nothing to stop us from calling the handler passed to THEN (even if we have 2 seconds). So, two seconds later, when the promise is resolved, the handler executes.

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer= > {
  setTimeout(() = > {
    console.log('Async task done');
    observer.next();
  }, 2000);
});
const subscription = observable.subscribe(() = > console.log('Handler'));
subscription.unsubscribe();
// logs:
// Async task done
Copy the code

We subscribe to the Observable, register a handler with it, and then unsubscribe from the Observable. As a result, our handler won’t be called 2 seconds later when the Observable will emit its value.

Note: Completed asynchronous tasks are still printed. Unsubscribing does not by itself mean that any asynchronous tasks that an Observable is performing will be aborted. Unsubscribe simply implements that calls to observer.next (as well as observer.error and observer.complete) in the subscriber function do not trigger calls to the handler function. But everything else will still work, as if unsubscribed.

Multicast and unicast

  • A promise’s executor function is executed only once (when a promise is created). This means that all calls to a given Promise object go directly into the executing executor function and end up with a copy of the value. Thus, a promise performs multicast because the same execution and result values are used for multiple subscribers.
  • The Observable subscriber function is executed on each call to subscribe to the Observable. Therefore, the observable performs unicast because each subscriber has separate execution and result values.

Promises:

const promise = new Promise(resolve= > {
  console.log('Executing... ');
  resolve(Math.random());
});
promise.then(result= > console.log(result));
promise.then(result= > console.log(result));
// logs:
// Executing...
/ / 0.1277775033205002
/ / 0.1277775033205002
Copy the code

As you can see, the Executor function is executed only once, and the resulting value is shared between the two subscriptions.

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer= > {
  console.log('Executing... ');
  observer.next(Math.random());
});
observable.subscribe(result= > console.log(result));
observable.subscribe(result= > console.log(result));
// logs:
// Executing...
/ / 0.9823994838399746
// Executing...
/ / 0.8877532356021958
Copy the code

As you can see, the subscriber function is executed separately for each subscriber, each with its own result value.

Asynchronous execution vs. synchronous execution

  • The promise handler is executed asynchronously. That is, they are executed after all the code in the main program or current function has been executed.
  • Observable handlers execute synchronously. That is, they are executed within the current function or main program flow.

Promises:

console.log('- Creating promise');
const promise = new Promise(resolve= > {
  console.log('- Promise running');
  resolve(1);
});
console.log('- Registering handler');
promise.then(result= > console.log('- Handling result: ' + result));
console.log('- Exiting main');
// logs:
// - Creating promise
// - Promise running
// - Registering handler
// - Exiting main
// - Handling result: 1
Copy the code

You first create a promise, and then execute the promise directly (because the promise executor function is pre-emptive, see above). Commitments were immediately resolved. After that, we register a handler by calling promise’s then method. At this point, the promise has been resolved (that is, it is in the completed state), however, our handler function has not yet been executed. Instead, we execute all the remaining code in the main program first, and then call our handler.

The reason is that promise completion (or rejection) is handled as an asynchronous event. This means that when a promise is parsed (or rejected), the corresponding handler is placed as a separate item in the JavaScript event queue. This means that the handler executes only after all previous items in the event queue have been executed, and in our example, one such previous item is the main program.

Observables:

import { Observable } from 'rxjs';

console.log('- Creating observable');
const observable = new Observable(observer= > {
  console.log('- Observable running');
  observer.next(1);
});
console.log('- Registering handler');
observable.subscribe(v= > console.log('- Handling result: ' + v));
console.log('- Exiting main');
// logs:
// - Creating observable
// - Registering handler
// - Observable running
// - Handling result: 1
// - Exiting main
Copy the code

First, an Observable is created (but it hasn’t been executed yet because an Observable is lazy, see above), and then we register a handler by calling the Subscribe method of the Observable. The Observable starts up and immediately emits its first and only value. Now that the handler is executed, the main program exits.

Unlike promises, handlers run while the main program is still running. This is because the Observable handler is called synchronously in the currently executing code, rather than as an asynchronous event like the Promise handler.

We studied the differences between Promises and Observables in terms of how easy it is to create, use, send data, destroy and execute data. You will find that Observables are better than Promises in every way. Is It true that Promises are not good? Promises have a killer async/await.

Promises and RxJS Observables interact with each other

Rxjs has a number of operators. Here are some common operators that can convert promises directly to Observables:

  • of
  • from
  • defer
  • forkJoin
  • concatMap
  • mergeMap
  • switchMap
  • exhaustMap
  • bufferToggle
  • audit
  • debounce
  • throttle
  • scheduled

Observable turns to a Promise with only two methods: toPromise and forEach.

We said before that async/await is a magic weapon of Promise, but async/await and Observables cannot really “work together”. We can accomplish this with the high interoperability of Observable and Promises.

If you accept an Observable, you accept a Promise

Many of the operators listed above can turn promises into Observables.

For example, if you are using a switchMap, you can return a Promise in it, just as you can return an Observable. All of this works:

import { interval, of } from 'rxjs';
import { mergeAll, take, map, switchMap } from 'rxjs/operators';

// Emit 10 times the observable value of 100 per second
const source$ = interval(1000).pipe(
  take(10),
  map(x= > x * 100));/** * returns a promise, waits for "ms" milliseconds and says "done" */
function promiseDelay(ms) {
  return new Promise(resolve= > {
    setTimeout(() = > resolve('done'), ms);
  });
}

/ / use switchMap
source$
  .pipe(switchMap(x= > promiseDelay(x))) / / callback
  .subscribe(x= > console.log('switchMap1', x));

source$
  .pipe(switchMap(promiseDelay)) // Make it simple
  .subscribe(x= > console.log('switchMap2', x));

// Or strange things you want to do
of(promiseDelay(100), promiseDelay(10000))
  .pipe(mergeAll())
  .subscribe(x= > console.log('of', x));
Copy the code

If you can access the function that creates the promise, you can wrap it with defer() and create an Observable that retries in case of an error.

import { defer } from 'rxjs';
import { retry } from 'rxjs/operators';

function getErringPromise() {
  console.log('getErringPromise called');
  return Promise.reject(new Error('sad'));
}

defer(getErringPromise)
  .pipe(retry(3))
  .subscribe(x= > console.log);
// logs
// getErringPromise called
// getErringPromise called
// getErringPromise called
// Error: sad
Copy the code

Defer turns out to be a very powerful operator. You can use this basically directly with the async/await function, which causes the Observable to emit the returned value and finish.

import { defer } from 'rxjs';

function promiseDelay(ms) {
  return new Promise(resolve= > {
    setTimeout(() = > resolve('done'), ms);
  });
}

defer(async function() {
  const a = await promiseDelay(1000).then(() = > 1);
  const b = a + (await promiseDelay(1000).then(() = > 2));
  return a + b + (await promiseDelay(1000).then(() = > 3));
}).subscribe(x= > console.log(x));
// logs:
/ / 7
Copy the code

There’s more than one way to subscribe to an Observable. There’s subscribe, which is the classic way to subscribe to an Observable. It returns a Subscription object, which can be used to unsubscribe, and forEach, This is an irrevocable way to subscribe to an Observable that requires a function for each next value and returns a Promise containing the Observable’s complete and error.

import { fromEvent } from 'rxjs';
import { take } from 'rxjs/operators';

const click$ = fromEvent(document.body, 'click');
function promiseDelay(ms) {
  return new Promise(resolve= > {
    setTimeout(() = > resolve('done'), ms);
  });
}

/** * Wait 5 clicks * Click complete to wait for the execution of promiseDelay */
async function doWork() {
  await click$.pipe(take(5)).forEach(i= > console.log(`click ${i}`));
  return await promiseDelay(1000);
}

doWork().then(v= > console.log(v));

// logs:
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// done
Copy the code

The toPromise function, like forEach, is an Observable method that subscribes to an Observable and wraps it into a Promise method. The Promise resolves to the last value released by the Observable when it completes. If the Observable never completes, then the Promise never resolves.

import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

const source$ = interval(1000).pipe(take(3)); / / 0, 1, 2
async function test() {
  return await source$.toPromise();
}

test().then(v= > console.log(v));
// logs:
/ / 2
Copy the code

Note: Using toPromise() is an anti-pattern, unless you are dealing with an API that expects a Promise, such as async/await. The Rxjs V7 version deprecates toPromise().

ForEach and toPromise both return promises but behave differently.

Existence is reasonable, technology is not good or bad, we need to develop strengths and circumvent weaknesses, use in combination, give full play to the greatest advantages of technology, write our most robust program.

Let’s call it a day, guys, have fun, and good luck!

Did you like this article? If so, please send me an ⭐.