In Angular 2, we came across a new concept, RxJS.
This can be a difficult place for many people to understand. Hard to understand is not because the API is complex, but because the concept of RxJS itself is hard to understand.
So, here’s a quick introduction to RxJS.
Functional Responsive Programming (FRP)
FRP has been proposed as early as the 1990s, but due to the limited capabilities of early compilers and runtimes, most programming practices tend to adopt the concept of human accommodating machine, namely imperative programming, or generalized process-oriented. (This refers to methods of programming in the form of specified steps, including object-oriented programming)
Another kind of programming is called declarative programming. Functional responsive programming is an important practice in declarative programming, where we do not specify steps for an operation, but simply give our intent.
In fact, declarative programming itself is not special, for example, our HTML is a very common declarative programming.
For example, we have the following HTML:
1
2Copy the code
For imperative programming, we might get something like this:
const ul = document.createElement('ul') ul.className = 'list'const lis = [1, 2].map(n => { li = document.createElement('li') li.className = 'item' li.textContent = n.toString() }) lis.forEach(li => ul.appendChild(li))Copy the code
By comparison, declarative programming can be much simpler than imperative programming in certain scenarios.
In addition to HTML, XML, CSS, SQL and Prolog are all declarative programming languages. For other general-purpose programming languages, although the language itself is mostly imperative, declarative programming practices can still be used in certain scenarios.
Reactive Extensions
Big hard method good!
Reactive Extensions is a functional responsive programming abstraction developed by Microsoft. It was first used in.NET (in the System.Reactive namespace but not in the standard library) and has since been extensively ported to other languages. For example, RxJS is a JavaScript port of Rx.
Although the languages are different, the core ideas of Rx and the main apis are still common, so what we have here applies equally to RxWhatever.
To explain functional responsiveness, let’s take a brief look at functional programming. In LoDash, for example, we can use method chains to implement complex operations:
interface Person {name: string, age: number}declare const people: Person[]
_(people)
.filter(person => person.age >= 18)
.forEach(person => console.log(`${person.name} is an adult.`))Copy the code
For general functional programming, we combine and transform some collection containers of data (Array, Map, Set, etc.), while in Rx, we deal with a special data collection called Observable, which can be regarded as an event flow.
Now, since each data item is an event, we don’t have all the events at the beginning, and we don’t know exactly when the events occurred.
For a very simple example, we still need to process the person above, but the person is not acquired simultaneously, but is created dynamically through user interaction. Of course, we could have written everything in the callback function, but that would have resulted in very high coupling and bad code quality.
So, we can make the creation of person here a stream of events and not care where it’s used or how it’s used.
class SomeComponent {
people: Subject
onCreate(person: Person) {
people.next(person)
}
}Copy the code
For real callers, it’s just a matter of getting the Observable and doing the rest.
const people$ = getPeopleObservableSomehow()
people$.filter(person => person.age >= 18)
.subscribe(person => console.log(`${person.name} is an adult.`))Copy the code
If we have other data sources, such as initial data from the server and subsequent data generated through interaction, we can do a combination:
const initialPeople = getPeopleSomehow()const people$ = getPeopleObservableSomehow()const allPeople$ = Observable.from(initialPeople)
.mergeMap(people$)
allPeople$.filter(person => person.age >= 18)
.subscribe(person => console.log(`${person.name} is an adult.`))Copy the code
In this way, our code can have high reusability, while greatly reducing the coupling and improving the quality of the code.
In addition, one of the major differences between functional responsive programming and its imperative counterpart is that functional responsive programming is push-based while imperative programming is (usually) pull-based. In other words, we don’t have values in functional responsive programming (we don’t get data by assigning).
The connection with Promise
We already know that one of the features of promises is composition and transformation. So what is the connection between Promises and Observables?
In fact, we can really think of an Observable as a Promise with a variable amount of data, whereas a Promise can contain only one data.
As a state machine, a Promise has three states: pending, Resolved and Rejected (if Cancelable Promise is officially approved, another state will be added). An Observable has N + 3 states: Idle, pending, resolved_0, resolved_1… Resolved_N, completed, and error.
Thus, an Observable can be both a finite state machine and an infinite state machine (N is infinite) compared to a finite state machine, Promise. In addition, Observables are subscriptible. For Cold Observables, they work only after they subscribe, and promises start to work as soon as they are generated.
Observables can be divided into Cold Observables and Hot Observables. Cold Observables start to generate events only after they are subscribed, such as observables encapsulated with Http. Hot Observables always generate events, such as an Observable that encapsulates an element’s Click event.
In addition, since the Promise has only one data, the Promise is completed when the data is acquired and only one state is required. An Observable, on the other hand, can have any number of data, so it needs an extra state to indicate completion, and no more data can be generated after completion.
In the current RxJS implementation, we can convert an Observable into a Promise using the.toPromise operator.
const aPromise = anObservable.toPromise()Copy the code
How to use loop syntax
We know that the iterative behavior achieved through callback functions can also be achieved through loops (e.g., for… Of and Array#forEach), can an Observable use loops?
It’s not recommended most of the time, but it’s totally feasible. For example, we can simply implement Iterable for Observable:
Observable[Symbol.iterator] = function () { let current = null let resolveCurrent = null let rejectCurrent = null let isDone = false function update() { current = new Promise((resolve, reject) => { resolveCurrent = resolve rejectCurrent = reject }) } update() this.subscribe(item => { resolveCurrent(item) update() }, error => { rejectCurrent(error) update() }, () => { isDone = true }) const next = () => ({ done: isDone, value: isDone ? null : current }) return { next } }Copy the code
In this way (only an idealized implementation, not strictly verified, should not be used directly in real projects), we can pass for… To use in a loop:
for (let itemPromise of someObservable) { let item = await itemPromise }Copy the code
However, we would still need to use await in the body of the loop, which is not pretty (it is pretty pretty, just for the purpose of saying so), so we can go one step further and implement Async Iterable directly (still a Stage 3 proposal, hopefully in ES2017) :
Observable[Symbol.asyncIterator] = function () { }Copy the code
Then we can loop in a simpler way:
for await (let item of someObservable) { }Copy the code
The operator
For promises, there is only one and only data, so there is no need for complex operations, just a simple transform (return value) or combine (return another Promise) function, or even combine transform and use into a single operation, our.then.
Observables can have any number of data, so many operators are provided to simplify user code for ease of use (see Array).
Similarly, there is no simple way to do it all:
-
For transformations, (the simplest way) use the.map method, which converts an element in an Observable to another form.
-
For composition, the (simplest).mergemap method is used to combine two Observables into one;
-
For usage, we need to use the.subscribe method to notify the Observer that we need it to start working.
Other operators are nothing more than a specific operation of transformation or combination. After understanding the basic principle of Observable, you can quickly understand an operator by carefully reading the document and corresponding dot plot.
Of course, there may be another class of operators, such as.toPromise, etc. These methods that do not return an Observable are not operators themselves, but merely archetypal extensions of an Observable.
Rx in Angular 2
In Angular 2, the EventEmitter is an Observable, because the @Output EventEmitter is a Subject. But while Angular 2 uses it, it’s not tied to our code, so if you don’t want to use Rx, you can ignore it altogether.
Another area where Rx is used is the Http module, which Observable serves as the interaction object for most of the API. Of course, Http itself is not necessary, it is only an official external extension, and there is no implementation specificity compared to other third party libraries (although the API design may be more uniform). So if you still don’t want to use an Observable, you can convert it to a Promise using.toPromise, or use a third-party Http Client like SuperAgent, or use the Fetch API directly. Because of zone.js, there is no need to wrap Angular accordingly.
In fact, Rx is also used in the Router module. For example, some apis in the ActivatedRoute interact in the way of Observables to realize dynamic responses. Of course, we could just use ActivatedRouteSnapshot so that we can directly process the data itself. The Router module also has no implementation specificity, and we can use the Angular 2 version of uI-Router if we wish.
However, as you can see from the above, Angular officially recommends Rx for the most part, and makes extensive use of Rx in API design to simplify the complexity. If you really want to develop team projects with Angular 2, it is worthwhile to know something about Rx. By contrast, not using Rx at all may actually require a higher learning cost.
conclusion
Knowledge of Rx is not required in Angular 2, it is mainly for API interaction, and it is recommended not only for Angular 2 use, but also for the value of functional responsive programming practices.
With an understanding of Iterable and AsyncIterable, it’s also possible to think of an Observable as a special case of AsyncIterable, that is, an asynchronous iterative container.
Rx provides a number of operators for combining and transforming Observables.