你有一份Rx程式設計祕籍請簽收

vivo網際網路技術發表於2021-06-15

一、背景

在學習Rx程式設計的過程中,理解Observable這個概念至關重要,常規學習過程中,通常需要進行多次“碰壁”才能逐漸“開悟”。這個有點像小時候學騎自行車,必須摔幾次才能掌握一樣。當然如果有辦法能“言傳”,則可以少走一些彎路,儘快領悟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!

四、快遞盒模型

4.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了。

4.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)的原因了——方便對他們進行統一操作!當然僅僅只是“開啟”(訂閱)這個操作只是最初級的功能,下面開始進階。

六、銷燬快遞盒

6.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() // 銷燬快遞盒

6.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這個快遞盒並非是高階函式,而是一個物件,但本質上是一樣的,這裡引出了一個話題:函數語言程式設計與物件導向的異同,請聽下回分解。

作者:vivo網際網路開發團隊-Li Yuxiang

相關文章