淺談Rx響應式編程

一、Observable

Observable從字面翻譯來說叫做“可觀察者”,換言之就是某種“數據源”或者“事件源”,這種數據源具有可被觀察的能力,這個和你主動去撈數據有本質區別。用一個形象的比喻就是Observable好比是水龍頭,你可以去打開水龍頭——訂閱Observable,然後水——數據就會源源不斷流出。這就是響應式編程的核心思想——變主動為被動。不過這個不在本篇文章中詳解。

Observable是一種概念,可以通過不同的方式去具體實現,本文通過高階函數來實現兩個常用Observable:fromEvent和Interval。通過講解對Observable的訂閱和取消訂閱兩個行為來幫助讀者真正理解Observable是什麼。

二、高階函數

高階函數的概念來源於函數式編程,簡單的定義就是一個函數的入參或者返回值是一個函數的函數。例如:

function foo(arg){
    return function(){
        console.log(arg)
    }
}
const bar = foo(“hello world”)
bar()  // hello world

ps:高階函數能做的事情很多,這裡僅僅針對本文需要的情形進行使用。

上面這個foo函數的調用並不會直接打印hello world,而隻是把這個hello world給緩存起來。後面我們根據實際需要調用返回出來的bar函數,然後真正去執行打印hello world的工作。

為啥要做這麼一步封裝呢?實際上這麼做的效果就是“延遲”瞭調用。而一切的精髓就在這個“延遲”兩個字裡面。我們實際上是對一種行為進行瞭包裝,看上去就像某種一致的東西,好比是快遞盒子。

裡面可以裝不同的東西,但對於物流來說就是統一的東西。因此,就可以形成對快遞盒的統一操作,比如堆疊、運輸、存儲、甚至是打開盒子這個動作也是一致的。

回到前面的例子,調用foo函數,相當於打包瞭一個快遞盒,這個快遞盒裡面有一個固定的程序,就是當打開這個快遞盒(調用bar)時執行一個打印操作。

我們可以有foo1、foo2、foo3……裡面有各種各樣的程序,但是這些foos,都有一個共同的操作就是“打開”。(前提是這個foo會返回一個函數,這樣才能滿足“打開”的操作,即調用返回的函數)。

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!

三、快遞盒模型

3.1、快遞盒模型1:fromEvent

有瞭上面的基礎,下面我們就來看一下Rx編程中最常用的一個Observable—fromEvent(……)。對於Rx編程的初學者,起初很難理解fromEvent(……)和addEventListener(……)有什麼區別。

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

如果直接執行這個代碼,確實效果是一樣的。那麼區別在哪兒呢?最直接的區別是,subscribe函數作用在fromEvent(……)上而不是btn上,而addEventListener是直接作用在btn上的。subscribe函數是某種“打開”操作,而fromEvent(……)則是某種快遞盒。

fromEvent實際上是對addEventListener的“延遲”調用

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

哦!fromEvent本質上是高階函數

至於如何實現subscribe來完成“打開”操作,不在本文討論范圍,在Rx編程中,這個subscribe的動作叫做“訂閱”。“訂閱”就是所有Observable的統一具備的操作。再次強調:本文中對Observable的“調用”在邏輯上相當於subscribe。

下面再舉一個例子,基本可以讓讀者舉二反N瞭。

3.2、快遞盒模型2:interval

Rx中有一個interval,它和setInterval有什麼區別呢?

估計有人已經開始搶答瞭,interval就是對setInterval的延遲調用!bingo!

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

從上面兩個例子來看,無論是fromEvent(……)還是Interval(……),雖然內部是完全不同的邏輯,但是他們同屬於“快遞盒”這種東西,我們把它稱之為Observable——可觀察者。

fromEvent和Interval本身隻是制作“快遞盒”的模型,隻有調用後返回的東西才是“快遞盒”,即fromEvent(btn,”click”)、interval(1000) 等等…

四、高階快遞盒

有瞭上面的基礎,下面開始進階:我們擁有瞭那麼多快遞盒,那麼就可以對這些快遞盒再封裝。

在文章開頭說瞭,快遞盒統一瞭一些操作,所以我們可以把許許多多的快遞盒堆疊在一起,即組合成一個大的快遞盒!這個大的快遞盒和小的快遞盒一樣,具有“打開”操作(即訂閱)。當我們打開這個大的快遞盒的時候,會發生什麼呢?

可以有很多種不同的可能性,比如可以逐個打開小的快遞盒(concat),或者一次性打開所有小的快遞盒(merge),也可以隻打開那個最容易打開的快遞盒(race)。

下面是一個簡化版的merge方法:

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

我們還是拿之前的fromEvent和interval來舉例吧!

使用merge方法對兩個Observable進行組合:

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

當我們“打開”(訂閱)這個大快遞盒ob的時候,其中兩個小快遞盒也會被“打開”(訂閱),任意一個小快遞盒裡面的邏輯都會被執行,我們就合並(merge)瞭兩個Observable,變成瞭一個。

這就是我們為什麼要辛辛苦苦把各種異步函數封裝成快遞盒(Observable)的原因瞭——方便對他們進行統一操作!當然僅僅隻是“打開”(訂閱)這個操作隻是最初級的功能,下面開始進階。

五、銷毀快遞盒

5.1、銷毀快遞盒——取消訂閱

我們還是以fromEvent為例子,之前我們寫瞭一個簡單的高階函數,作為對addEventListener的封裝:

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

當我們調用這個函數的時候,就生成瞭一個快遞盒(fromEvent(btn,’click’))。當我們調用瞭這個函數返回的函數的時候,就是打開瞭快遞盒(fromEvent(btn,’click’)(console.log))。

那麼我們怎麼去銷毀這個打開的快遞盒呢?

首先我們需要得到一個已經打開的快遞盒,上面的函數調用結果是void,我們無法做任何操作,所以我們需要構造出一個打開狀態的快遞盒。還是使用高階函數的思想:在返回的函數裡面再返回一個函數,用於銷毀操作。

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() // 銷毀快遞盒

同理,對於interval,我們也可以如法炮制:

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() // 銷毀快遞盒

5.2、銷毀高階快遞盒

我們以merge為例:

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() // 銷毀大快遞盒

當我們銷毀大快遞盒的時候,就會把裡面所有的小快遞盒一起銷毀。

六、補充

到這裡我們已經將Observable的兩個重要操作(訂閱、取消訂閱)講完瞭,值得註意的是,取消訂閱這個行為並非是作用於Observable上,而是作用於已經“打開”的快遞盒(訂閱Observable後返回的東西)之上!

Observable除此以外,還有兩個重要操作,即發出事件、完成/異常,(這兩個操作屬於是由Observable主動發起的回調,和操作的方向是相反的,所以其實不能稱之為操作)。

這個兩個行為用快遞盒就不那麼形象瞭,我們可以將Observable比做是水龍頭,原先的打開快遞盒變成擰開水龍頭,而我們傳入的回調函數就可以比喻成接水的水杯!由於大傢對回調函數已經非常熟悉瞭,所以本文就不再贅述瞭。

七、後記

總結一下我們學習的內容,我們通過高階函數將一些操作進行瞭“延遲”,並賦予瞭統一的行為,比如“訂閱”就是延遲執行瞭異步函數,“取消訂閱”就是在上面的基礎上再“延遲”執行瞭銷毀資源的函數。

這些所謂的“延遲”執行就是Rx編程中幕後最難理解,也是最核心的部分。Rx的本質就是將異步函數封裝起來,然後抽象成四大行為:訂閱、取消訂閱、發出事件、完成/異常。

實際實現Rx庫的方法有很多,本文隻是利用瞭高階函數的思想來幫助大傢理解Observable的本質,在官方實現的版本中,Observable這個快遞盒並非是高階函數,而是一個對象,但本質上是一樣的

以上就是淺談Rx響應式編程的詳細內容,更多關於Rx響應式編程的資料請關註WalkonNet其它相關文章!

推薦閱讀: