淺談Rx響應(yīng)式編程
一、Observable
Observable從字面翻譯來(lái)說(shuō)叫做“可觀察者”,換言之就是某種“數(shù)據(jù)源”或者“事件源”,這種數(shù)據(jù)源具有可被觀察的能力,這個(gè)和你主動(dòng)去撈數(shù)據(jù)有本質(zhì)區(qū)別。用一個(gè)形象的比喻就是Observable好比是水龍頭,你可以去打開(kāi)水龍頭——訂閱Observable,然后水——數(shù)據(jù)就會(huì)源源不斷流出。這就是響應(yīng)式編程的核心思想——變主動(dòng)為被動(dòng)。不過(guò)這個(gè)不在本篇文章中詳解。
Observable是一種概念,可以通過(guò)不同的方式去具體實(shí)現(xiàn),本文通過(guò)高階函數(shù)來(lái)實(shí)現(xiàn)兩個(gè)常用Observable:fromEvent和Interval。通過(guò)講解對(duì)Observable的訂閱和取消訂閱兩個(gè)行為來(lái)幫助讀者真正理解Observable是什么。
二、高階函數(shù)
高階函數(shù)的概念來(lái)源于函數(shù)式編程,簡(jiǎn)單的定義就是一個(gè)函數(shù)的入?yún)⒒蛘叻祷刂凳且粋€(gè)函數(shù)的函數(shù)。例如:
function foo(arg){ return function(){ console.log(arg) } } const bar = foo(“hello world”) bar() // hello world
ps:高階函數(shù)能做的事情很多,這里僅僅針對(duì)本文需要的情形進(jìn)行使用。
上面這個(gè)foo函數(shù)的調(diào)用并不會(huì)直接打印hello world,而只是把這個(gè)hello world給緩存起來(lái)。后面我們根據(jù)實(shí)際需要調(diào)用返回出來(lái)的bar函數(shù),然后真正去執(zhí)行打印hello world的工作。
為啥要做這么一步封裝呢?實(shí)際上這么做的效果就是“延遲”了調(diào)用。而一切的精髓就在這個(gè)“延遲”兩個(gè)字里面。我們實(shí)際上是對(duì)一種行為進(jìn)行了包裝,看上去就像某種一致的東西,好比是快遞盒子。
里面可以裝不同的東西,但對(duì)于物流來(lái)說(shuō)就是統(tǒng)一的東西。因此,就可以形成對(duì)快遞盒的統(tǒng)一操作,比如堆疊、運(yùn)輸、存儲(chǔ)、甚至是打開(kāi)盒子這個(gè)動(dòng)作也是一致的。
回到前面的例子,調(diào)用foo函數(shù),相當(dāng)于打包了一個(gè)快遞盒,這個(gè)快遞盒里面有一個(gè)固定的程序,就是當(dāng)打開(kāi)這個(gè)快遞盒(調(diào)用bar)時(shí)執(zhí)行一個(gè)打印操作。
我們可以有foo1、foo2、foo3……里面有各種各樣的程序,但是這些foos,都有一個(gè)共同的操作就是“打開(kāi)”。(前提是這個(gè)foo會(huì)返回一個(gè)函數(shù),這樣才能滿足“打開(kāi)”的操作,即調(diào)用返回的函數(shù))。
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
有了上面的基礎(chǔ),下面我們就來(lái)看一下Rx編程中最常用的一個(gè)Observable—fromEvent(……)。對(duì)于Rx編程的初學(xué)者,起初很難理解fromEvent(……)和addEventListener(……)有什么區(qū)別。
btn.addEventListener("click",callback) rx.fromEvent(btn,"click").subscribe(callback)
如果直接執(zhí)行這個(gè)代碼,確實(shí)效果是一樣的。那么區(qū)別在哪兒呢?最直接的區(qū)別是,subscribe函數(shù)作用在fromEvent(……)上而不是btn上,而addEventListener是直接作用在btn上的。subscribe函數(shù)是某種“打開(kāi)”操作,而fromEvent(……)則是某種快遞盒。
fromEvent實(shí)際上是對(duì)addEventListener的“延遲”調(diào)用
function fromEvent(target,evtName){ return function(callback){ target.addEventListener(evtName,callback) } } const ob = fromEvent(btn,"click") ob(console.log)// 相當(dāng)于 subscribe
哦!fromEvent本質(zhì)上是高階函數(shù)
至于如何實(shí)現(xiàn)subscribe來(lái)完成“打開(kāi)”操作,不在本文討論范圍,在Rx編程中,這個(gè)subscribe的動(dòng)作叫做“訂閱”?!坝嗛啞本褪撬蠴bservable的統(tǒng)一具備的操作。再次強(qiáng)調(diào):本文中對(duì)Observable的“調(diào)用”在邏輯上相當(dāng)于subscribe。
下面再舉一個(gè)例子,基本可以讓讀者舉二反N了。
3.2、快遞盒模型2:interval
Rx中有一個(gè)interval,它和setInterval有什么區(qū)別呢?
估計(jì)有人已經(jīng)開(kāi)始搶答了,interval就是對(duì)setInterval的延遲調(diào)用!bingo!
function interval(period){ let i = 0 return function(callback){ setInterval(period,()=>callback(i++)) } } const ob = interval(1000) ob(console.log)// 相當(dāng)于 subscribe
從上面兩個(gè)例子來(lái)看,無(wú)論是fromEvent(……)還是Interval(……),雖然內(nèi)部是完全不同的邏輯,但是他們同屬于“快遞盒”這種東西,我們把它稱之為Observable——可觀察者。
fromEvent和Interval本身只是制作“快遞盒”的模型,只有調(diào)用后返回的東西才是“快遞盒”,即fromEvent(btn,"click")、interval(1000) 等等...
四、高階快遞盒
有了上面的基礎(chǔ),下面開(kāi)始進(jìn)階:我們擁有了那么多快遞盒,那么就可以對(duì)這些快遞盒再封裝。
在文章開(kāi)頭說(shuō)了,快遞盒統(tǒng)一了一些操作,所以我們可以把許許多多的快遞盒堆疊在一起,即組合成一個(gè)大的快遞盒!這個(gè)大的快遞盒和小的快遞盒一樣,具有“打開(kāi)”操作(即訂閱)。當(dāng)我們打開(kāi)這個(gè)大的快遞盒的時(shí)候,會(huì)發(fā)生什么呢?
可以有很多種不同的可能性,比如可以逐個(gè)打開(kāi)小的快遞盒(concat),或者一次性打開(kāi)所有小的快遞盒(merge),也可以只打開(kāi)那個(gè)最容易打開(kāi)的快遞盒(race)。
下面是一個(gè)簡(jiǎn)化版的merge方法:
function merge(...obs){ return function(callback){ obs.forEach(ob=>ob(callback)) // 打開(kāi)所有快遞盒 } }
我們還是拿之前的fromEvent和interval來(lái)舉例吧!
使用merge方法對(duì)兩個(gè)Observable進(jìn)行組合:
const ob1 = fromEvent(btn,'click') // 制作快遞盒1 const ob2 = interval(1000) // 制作快遞盒2 const ob = merge(ob1,ob2) //制作大快遞盒 ob(console.log) // 打開(kāi)大快遞盒
當(dāng)我們“打開(kāi)”(訂閱)這個(gè)大快遞盒ob的時(shí)候,其中兩個(gè)小快遞盒也會(huì)被“打開(kāi)”(訂閱),任意一個(gè)小快遞盒里面的邏輯都會(huì)被執(zhí)行,我們就合并(merge)了兩個(gè)Observable,變成了一個(gè)。
這就是我們?yōu)槭裁匆列量嗫喟迅鞣N異步函數(shù)封裝成快遞盒(Observable)的原因了——方便對(duì)他們進(jìn)行統(tǒng)一操作!當(dāng)然僅僅只是“打開(kāi)”(訂閱)這個(gè)操作只是最初級(jí)的功能,下面開(kāi)始進(jìn)階。
五、銷(xiāo)毀快遞盒
5.1、銷(xiāo)毀快遞盒——取消訂閱
我們還是以fromEvent為例子,之前我們寫(xiě)了一個(gè)簡(jiǎn)單的高階函數(shù),作為對(duì)addEventListener的封裝:
function fromEvent(target,evtName){ return function(callback){ target.addEventListener(evtName,callback) } }
當(dāng)我們調(diào)用這個(gè)函數(shù)的時(shí)候,就生成了一個(gè)快遞盒(fromEvent(btn,'click'))。當(dāng)我們調(diào)用了這個(gè)函數(shù)返回的函數(shù)的時(shí)候,就是打開(kāi)了快遞盒(fromEvent(btn,'click')(console.log))。
那么我們?cè)趺慈ヤN(xiāo)毀這個(gè)打開(kāi)的快遞盒呢?
首先我們需要得到一個(gè)已經(jīng)打開(kāi)的快遞盒,上面的函數(shù)調(diào)用結(jié)果是void,我們無(wú)法做任何操作,所以我們需要構(gòu)造出一個(gè)打開(kāi)狀態(tài)的快遞盒。還是使用高階函數(shù)的思想:在返回的函數(shù)里面再返回一個(gè)函數(shù),用于銷(xiāo)毀操作。
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) // 打開(kāi)快遞盒,并得到一個(gè)可用于銷(xiāo)毀的函數(shù) sub() // 銷(xiāo)毀快遞盒
同理,對(duì)于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) // 打開(kāi)快遞盒 sub() // 銷(xiāo)毀快遞盒
5.2、銷(xiāo)毀高階快遞盒
我們以merge為例:
function merge(...obs){ return function(callback){ const subs = obs.map(ob=>ob(callback)) // 訂閱所有并收集所有的銷(xiāo)毀函數(shù) return function(){ subs.forEach(sub=>sub()) // 遍歷銷(xiāo)毀函數(shù)并執(zhí)行 } } } const ob1 = fromEvent(btn,'click') // 制作快遞盒1 const ob2 = interval(1000) // 制作快遞盒2 const ob = merge(ob1,ob2) //制作大快遞盒 const sub = ob(console.log) // 打開(kāi)大快遞盒 sub() // 銷(xiāo)毀大快遞盒
當(dāng)我們銷(xiāo)毀大快遞盒的時(shí)候,就會(huì)把里面所有的小快遞盒一起銷(xiāo)毀。
六、補(bǔ)充
到這里我們已經(jīng)將Observable的兩個(gè)重要操作(訂閱、取消訂閱)講完了,值得注意的是,取消訂閱這個(gè)行為并非是作用于Observable上,而是作用于已經(jīng)“打開(kāi)”的快遞盒(訂閱Observable后返回的東西)之上!
Observable除此以外,還有兩個(gè)重要操作,即發(fā)出事件、完成/異常,(這兩個(gè)操作屬于是由Observable主動(dòng)發(fā)起的回調(diào),和操作的方向是相反的,所以其實(shí)不能稱之為操作)。
這個(gè)兩個(gè)行為用快遞盒就不那么形象了,我們可以將Observable比做是水龍頭,原先的打開(kāi)快遞盒變成擰開(kāi)水龍頭,而我們傳入的回調(diào)函數(shù)就可以比喻成接水的水杯!由于大家對(duì)回調(diào)函數(shù)已經(jīng)非常熟悉了,所以本文就不再贅述了。
七、后記
總結(jié)一下我們學(xué)習(xí)的內(nèi)容,我們通過(guò)高階函數(shù)將一些操作進(jìn)行了“延遲”,并賦予了統(tǒng)一的行為,比如“訂閱”就是延遲執(zhí)行了異步函數(shù),“取消訂閱”就是在上面的基礎(chǔ)上再“延遲”執(zhí)行了銷(xiāo)毀資源的函數(shù)。
這些所謂的“延遲”執(zhí)行就是Rx編程中幕后最難理解,也是最核心的部分。Rx的本質(zhì)就是將異步函數(shù)封裝起來(lái),然后抽象成四大行為:訂閱、取消訂閱、發(fā)出事件、完成/異常。
實(shí)際實(shí)現(xiàn)Rx庫(kù)的方法有很多,本文只是利用了高階函數(shù)的思想來(lái)幫助大家理解Observable的本質(zhì),在官方實(shí)現(xiàn)的版本中,Observable這個(gè)快遞盒并非是高階函數(shù),而是一個(gè)對(duì)象,但本質(zhì)上是一樣的
以上就是淺談Rx響應(yīng)式編程的詳細(xì)內(nèi)容,更多關(guān)于Rx響應(yīng)式編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中添加監(jiān)聽(tīng)句柄的方式
這篇文章主要介紹了JavaScript中添加監(jiān)聽(tīng)句柄的方式,監(jiān)聽(tīng)就是觸發(fā)某事件之后做出的響應(yīng),監(jiān)聽(tīng)句柄是觸發(fā)某相應(yīng)的條件,下面關(guān)于添加監(jiān)聽(tīng)句柄的方式的詳細(xì)內(nèi)容,需要的朋友可以參考一下,希望對(duì)你有所幫助2022-02-02JavaScript中的子窗口與父窗口的互相調(diào)用問(wèn)題
本文給大家介紹了JavaScript中的子窗口與父窗口的互相調(diào)用問(wèn)題,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-02-02JavaScript不刷新實(shí)現(xiàn)瀏覽器的前進(jìn)后退功能
這篇文章主要介紹了JavaScript不刷新實(shí)現(xiàn)瀏覽器的前進(jìn)后退功能,本文給出了HTML5解決方案、老舊瀏覽器的寫(xiě)法等方法,需要的朋友可以參考下2014-11-11使用JS前端加密庫(kù)sm-crypto實(shí)現(xiàn)國(guó)密sm2、sm3和sm4加密與解密
這篇文章主要介紹了使用JS前端加密庫(kù)sm-crypto實(shí)現(xiàn)國(guó)密sm2、sm3和sm4加密與解密,需要的朋友可以參考下2024-06-06微信小程序?qū)崿F(xiàn)簡(jiǎn)單的搖骰子游戲
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)簡(jiǎn)單的搖骰子游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05JavaScript實(shí)現(xiàn)日期格式化詳細(xì)實(shí)例
這篇文章主要給大家介紹了關(guān)于JavaScript實(shí)現(xiàn)日期格式化的相關(guān)資料,JavaScript中的日期對(duì)象提供了許多方法和屬性,可以通過(guò)它們來(lái)進(jìn)行日期的格式化,需要的朋友可以參考下2023-09-09JS實(shí)現(xiàn)跟隨鼠標(biāo)的鏈接文字提示框效果
這篇文章主要介紹了JS實(shí)現(xiàn)跟隨鼠標(biāo)的鏈接文字提示框效果,涉及javascript鼠標(biāo)事件及頁(yè)面元素樣式操作的相關(guān)技巧,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2015-08-08