preface

This is a new series that documents your own learning of RxJS and functional programming. The purpose of this series is to take notes and to get you interested in learning/using RxJS and functional programming.

We learned RxJS not because it’s a new technology (it’s been around for years) or because it’s a cool technology, but because it really solves a lot of problems:

  • How do you control the complexity of large amounts of code
  • How do I keep my code readable
  • How do you handle asynchronous operations

Many of you have probably heard of RxJS and its daunting learning curve (not you), but don’t be afraid that this series will explain it to you in the most understandable way possible (I won’t either). To borrow a phrase from the RxJS primer:

Think of RxJS as Lodash for handling events.

If there is no special explanation, RxJS version is based on 7.3.0 from this issue. This series of content is based on RxJS written by Cheng Mo (good book recommendation), and has been updated and summarized to some extent. Some quotes may be quoted from the book.

These reviews

  • RxJS and Functional Programming – Functional programming
  • RxJS and Functional Programming – Getting started with RxJS

Operator basics

An operator is a function that returns an Observable. However, some operators generate an Observable that returns from another Observable, and some operators generate an Observable that returns from other types of input. There are also operators that create an Observable out of thin air without input.

Using and combining operators is an important part of RxJS programming, and it is no exaggeration to say that proficiency in using operators determines proficiency in RxJS.

First look at a simple chestnut 🌰:

import { of } from "rxjs";

const source$ = of(1.2.3);

source$.subscribe(console.log);
Copy the code

Of converts its parameters into an observable sequence.

The sequence is modified using the map operator, similar to the JS map operator, which also modifies each item of the sequence and returns the modified data.

import { of } from "rxjs";
import { map } from "rxjs/operators";

const source$ = of(1.2.3);

source$.pipe(map((value) = > value * 2)).subscribe(console.log);
Copy the code

Pipe and map operators are introduced in the above code. Pipe replaces the chain operation of the import patch operator, on the one hand, it solves the problem of increasing Observable. On the one hand, tree-shaking can greatly reduce packaging volume at the engineering level.

Pipe can pass in any desired combination of operators, such as the code above that increments each number by 2:

import { of } from "rxjs";
import { map } from "rxjs/operators";

const source$ = of(1.2.3);

source$
  .pipe(
    map((value) = > value * 2),
    map((value) = > value + 1)
  )
  .subscribe(console.log);
Copy the code

You can open CodesandBox and see the results directly

Looking at a filter operator, imagine a requirement that prints all the even numbers in a given sequence:

import { of } from "rxjs";
import { map, filter } from "rxjs/operators";

const source$ = of(1.2.3);

source$
  .pipe(
    filter((value) = > value % 2= = =0),
    map((value) = > 'The even numbers are:${value}`)
  )
  .subscribe(console.log);
Copy the code

You can open CodesandBox and see the results directly

Let’s take a more complicated example:

Implement a draggable button in the middle of the screen. As it moves toward the edge, the background color changes from white to red.

<div id="overlay"></div>
<div id="button" draggable="true"></div>
Copy the code
body {
  overflow: hidden;
  margin: 0;
}

#overlay {
  width: 100vw;
  height: 100vh;
  opacity: 0;
  background: red;
}

#button {
  cursor: grabbing;
  background-color: black;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
Copy the code
import "./styles.css";

import { fromEvent } from "rxjs";
import { map, tap } from "rxjs/operators";

const button = document.querySelector("#button");
const overlay = document.querySelector("#overlay");
const maxY = window.innerHeight / 2;
const maxX = window.innerWidth / 2;

fromEvent(button, "drag")
  .pipe(
    // Calculate opacity of overlay
    map((event) = > {
      if (event.clientY === 0 && event.clientX === 0) {
        return 0;
      }

      const y = Math.abs(event.clientY - maxY);
      const pY = y / maxY;
      const x = Math.abs(event.clientX - maxX);
      const pX = x / maxX;
      return Math.max(pY, pX);
    }),
    The tap operator is typically used to perform side effects on data
    tap((opacity) = > {
      overlay.style.opacity = opacity;
    })
  )
  .subscribe(console.log);
Copy the code

You can open CodesandBox and see the results directly

Classification of operators

The most difficult part of mastering operators is that when encountering a practical problem, which one or some operators should be selected to solve the problem. Therefore, it is necessary to classify these operators first and know the characteristics of various operators.

Operators can be divided into the following categories based on functionality:

  • Creating a class
  • Transformation class
  • Filtering
  • Combination of classes
  • Multicasting
  • Error Handling
  • Utility Class
  • Conditional&boolean (conditional&Boolean)
  • Mathematics and Aggregate (Mathmatical&Aggregate)

Here’s how the operator is implemented. Although the main focus for application developers is how to use operators in RxJS, understanding how operators are implemented will help you understand RxJS better. In addition, while not everyone adds operators to the RxJS code base, there is a good chance that every application project will use reusable logos that can be encapsulated in custom operators, and you will need to know how to customize a new operator.

Create operator

Each operator is a function, and regardless of what function is implemented, the following functional points must be considered:

  • Returns a brand new Observable.
  • Upstream and downstream subscription and unsubscribe processing.
  • Handle exceptions.
  • Release resources in a timely manner.

The simplest implementation of the Map operator illustrates the above points.

Returns a brand new Observable

function map(project) {
  return (source) = >
    new Observable((observer) = >
      source.subscribe({
        next: (value) = > observer.next(project(value)),
        error: (err) = > observer.error(err),
        complete: () = > observer.complete(),
      })
    );
}
Copy the code

This completes a basic map operator, but it is not a complete operator

Upstream and downstream subscription and unsubscribe processing

The above implementation subscribed to the source but did not process unsubscribe, which could result in resource leaks if related resources are not released. We improved the code:

function map(project) {
  return (source) = >
    new Observable((observer) = > {
      const sub = source.subscribe({
        next: (value) = > observer.next(project(value)),
        error: (err) = > observer.error(err),
        complete: () = > observer.complete(),
      });
      return {
        unsubscribe: sub.unsubscribe,
      };
    });
}
Copy the code

Handling exceptions

For the map operator, the parameter project is an input that is not controlled by the map itself. In other words, project may be problematic code that is beyond the control of the Map function.

Improve the map implementation above as follows:

function map(project) {
  return (source) = >
    new Observable((observer) = > {
      const sub = source.subscribe({
        next: (value) = > {
          try {
            observer.next(project(value));
          } catch(err) { observer.error(err); }},error: (err) = > observer.error(err),
        complete: () = > observer.complete(),
      });
      return {
        unsubscribe: sub.unsubscribe,
      };
    });
}
Copy the code

Project uses a try catch to catch possible errors at call time and calls the downstream error function if an error occurs.

Therefore, map has two possible ways to transmit error messages to the downstream. One is that the upstream error is directly handed to the downstream, and the other is that the error generated during the execution of the project function is also handed to the downstream.

Releasing resources in time

Maps have no resource footprint, but some operators do not, especially those that work directly with browser resources. For example, when the generated Observable is subscribed, it must add event handlers to the DOM. If event handlers are added but not deleted, there is a risk of resource leakage. Be sure to remove these event handlers from the DOM when unsubscribing. Operators that associate WebSocket resources to get push messages must release WebSocket resources when the associated Observable is unsubscribed.

We now have the same map operator that RXJS officially provides.

conclusion

This article introduces the use of RxJS operators and the implementation of custom operators.

The next chapter covers a few operators for creating data flows