头图

1. Background

In the process of learning Rx programming, it is very important to understand the concept of Observable. In the normal learning process, it is usually necessary to "hit the wall" many times to gradually "enlighten". This is a bit like learning to ride a bicycle when I was young, and I had to fall a few times to master it. Of course, if there is a way to "rule", you can avoid some detours and understand the subtleties of Rx as soon as possible.

Two, Observable

Observable is called "observable" in literal translation. In other words, it is some kind of "data source" or "event source". This kind of data source has the ability to be observed. This is essentially different from your initiative to collect data. A vivid analogy is that Observable is like a faucet. You can turn on the faucet-subscribe to Observable, and then water-data will flow out continuously. This is the core idea of reactive programming-change from active to passive. But this is not explained in detail in this article.

(The picture comes from the Internet)

Observable is a concept that can be implemented in different ways. This article uses higher-order functions to implement two commonly used Observables: fromEvent and Interval. By explaining the two behaviors of Observable subscription and unsubscription to help readers really understand what Observable is.

Three, higher-order functions

The concept of higher-order functions comes from functional programming. The simple definition is a function whose input parameter or return value is a function. E.g:

function foo(arg){
    return function(){
        console.log(arg)
    }
}
const bar = foo(“hello world”)
bar()  // hello world
ps: There are many things that higher-order functions can do, and they are only used here for the situations that this article needs.

The above call to the foo function does not directly print the hello world, but only caches the hello world. Later, we call the returned bar function according to actual needs, and then actually perform the job of printing hello world.

Why do you want to do such a one-step package? In fact, the effect of doing so is to "delay" the call. And the essence of everything lies in the word "delay". We actually package a behavior that looks like something consistent, like a courier box.

(The picture comes from the Internet)

It can contain different things, but it is a unified thing for logistics. Therefore, a unified operation of express boxes can be formed, such as stacking, transporting, storing, and even opening the box. The actions are also consistent.

Going back to the previous example, calling the foo function is equivalent to packaging a courier box. There is a fixed program in the courier box, which is to perform a printing operation when the courier box is opened (call bar).

We can have foo1, foo2, foo3...There are various programs in it, but these foos have a common operation that is "open". (The premise is that this foo will return a function, so as to satisfy the "open" operation, that is, call the returned function).

function foo1(arg){
    return function(){
       console.log(arg+"?")
    }
}
function foo2(arg){
      return function(){
         console.log(arg+"!")
     }
}
const bar1 = foo1(“hello world”)
const bar2 = foo2("yes")
bar1()+bar2() // hello world? yes!

Four, express box model

4.1 Express box model 1: fromEvent

With the above foundation, let's take a look at one of the most commonly used Observable—fromEvent(……) in Rx programming. For beginners of Rx programming, it is difficult to understand the difference between fromEvent(……) and addEventListener(……) at first.

btn.addEventListener("click",callback)
rx.fromEvent(btn,"click").subscribe(callback)

If you execute this code directly, the effect is indeed the same. So where is the difference? The most direct difference is that the subscribe function works on fromEvent(...) instead of btn, while addEventListener works directly on btn. The subscribe function is a kind of "open" operation, and fromEvent(...) is a kind of express box.

fromEvent is actually a "delayed" call to addEventListener

function fromEvent(target,evtName){
    return function(callback){
        target.addEventListener(evtName,callback)
    }
}
const ob = fromEvent(btn,"click")
ob(console.log)// 相当于 subscribe

Oh! fromEvent is essentially a higher-order function

As for how to implement subscribe to complete the "open" operation, it is beyond the scope of this article. In Rx programming, this subscribe action is called "subscribe". "Subscription" is the unified operation of all Observables. Again: the "call" of Observable in this article is logically equivalent to subscribe.

Here is another example, which basically allows readers to cite the opposite.

4.2 Express box model 2: interval

There is an interval in Rx. What is the difference between it and setInterval?

It is estimated that some people have already started to answer, interval is a delayed call to setInterval! bingo!

function interval(period){
    let i = 0
    return function(callback){
        setInterval(period,()=>callback(i++))
    }
}
const ob = interval(1000)
ob(console.log)// 相当于 subscribe

From the above two examples, whether it is fromEvent(……) or Interval(……), although the internal logic is completely different, they belong to the "express box" and we call it Observable —— .

fromEvent and Interval itself are just models for making "express box", only what is returned after the call is the "express box", that is, fromEvent(btn,"click"), interval(1000), etc...

Five, high-end express box

With the above foundation, the next step is to advance: we have so many express boxes, then these express boxes can be repackaged.

As mentioned at the beginning of the article, the express box unifies some operations, so we can stack many express boxes together to form a big express box! This big courier box, like the small courier box, has an "open" operation (that is, subscription). What happens when we open this big courier box?

There can be many different possibilities. For example, you can open the small courier boxes one by one (concat), or open all the small courier boxes at once (merge), or you can open only the easiest courier box (race).

Here is a simplified version of the merge method:

function merge(...obs){
    return function(callback){
        obs.forEach(ob=>ob(callback)) // 打开所有快递盒
    }
}

Let's take the previous fromEvent and interval as examples!

Use the merge method to combine two Observables:

const ob1 = fromEvent(btn,'click') // 制作快递盒1
const ob2 = interval(1000) // 制作快递盒2
const ob = merge(ob1,ob2) //制作大快递盒
ob(console.log) // 打开大快递盒

When we "open" (subscribe) this big courier box ob, two of the small courier boxes will also be "opened" (subscribe), the logic in any small courier box will be executed, and we will merge (merge) Two Observables became one.

This is why we have to work so hard to encapsulate various asynchronous functions into Observables-to facilitate unified operations on them! Of course, just "open" (subscribe) this operation is only the most basic function, let's start to advance.

6. Destroy the courier box

6.1 Destroy the courier box-cancel subscription

Let's take fromEvent as an example. Before, we wrote a simple high-order function as an encapsulation for addEventListener:

function fromEvent(target,evtName){
    return function(callback){
        target.addEventListener(evtName,callback)
    }
}

When we call this function, an express box (fromEvent(btn,'click')) is generated. When we call the function returned by this function, the express box (fromEvent(btn,'click')(console.log)) is opened.

So how do we destroy this opened express box?

First, we need to get an opened express box. The result of the above function call is void. We can't do anything, so we need to construct an open express box. Still use the idea of higher-order functions: return a function in the returned function for the destruction operation.

function fromEvent(target,evtName){
    return function(callback){
        target.addEventListener(evtName,callback)
        return function(){
            target.removeEventListener(evtName,callback)
        }
    }
}
const ob = fromEvent(btn,'click') // 制作快递盒
const sub = ob(console.log) // 打开快递盒,并得到一个可用于销毁的函数
sub() // 销毁快递盒

In the same way, for interval, we can do the same:

function interval(period){
    let i = 0
    return function(callback){
        let id = setInterval(period,()=>callback(i++))
        return function(){
            clearInterval(id)
        }
    }
}
const ob = interval(1000) // 制作快递盒
const sub = ob(console.log) // 打开快递盒
sub() // 销毁快递盒

6.2 Destroy high-end courier boxes

Let's take merge as an example:

function merge(...obs){
    return function(callback){
        const subs = obs.map(ob=>ob(callback)) // 订阅所有并收集所有的销毁函数
        return function(){
            subs.forEach(sub=>sub()) // 遍历销毁函数并执行
        }
    }
}
 
const ob1 = fromEvent(btn,'click') // 制作快递盒1
const ob2 = interval(1000) // 制作快递盒2
const ob = merge(ob1,ob2) //制作大快递盒
const sub = ob(console.log) // 打开大快递盒
sub() // 销毁大快递盒

When we destroy the big courier box, we will destroy all the small courier boxes inside.

Six, supplement

At this point, we have finished the two important operations of Observable (subscribe, unsubscribe). It is worth noting that the behavior of unsubscribing is not on the Observable, but on the courier box that has been "opened" (subscribe to Observable). After returning something) above!

In addition to this, Observable also has two important operations, namely event emission and completion/exception. (These two operations belong to the callback initiated by Observable, and the direction of the operation is opposite, so they can’t be called operations in fact) .

These two behaviors are not so vivid with the express box. We can compare the Observable to a faucet. The original opening of the express box becomes an unscrewed faucet, and the callback function we pass in can be compared to a water cup! Since everyone is already very familiar with the callback function, this article will not go into details.

Seven, postscript

Summarizing what we have learned, we have "delayed" some operations through higher-order functions, and given uniform behaviors. For example, "subscription" means delaying the execution of asynchronous functions, and "unsubscription" means adding to the above. "Delayed" the function of destroying resources is executed.

These so-called "delayed" executions are the most difficult to understand behind the scenes in Rx programming, and they are also the core part. The essence of Rx is to encapsulate asynchronous functions, and then abstract them into four major behaviors: subscribe, unsubscribe, issue events, and complete/exception.

There are many ways to actually implement the Rx library. This article just uses the idea of higher-order functions to help you understand the essence of Observable. In the official version, the express box of Observable is not a higher-order function, but an object, but the essence The above is the same. Here comes a topic: the similarities and differences between functional programming and object-oriented, please listen to the next time.

Author: vivo Internet Development Team-Li Yuxiang

vivo互联网技术
3.3k 声望10.2k 粉丝