這是一篇給新手的 RxJS 快速入門,它可能不精確、不全面,但力求對新手友好。
異步與“回調(diào)地獄”
我們都知道 JavaScript 是個(gè)多范式語言,它既支持過程式編程,又支持函數(shù)式編程,兩者分別適用于不同的場合。在同步環(huán)境下,兩者各有優(yōu)缺點(diǎn),甚至有時(shí)候過程式會更簡明一些,但在異步環(huán)境下(最典型的場景是一個(gè) Ajax 請求完成后緊接著執(zhí)行另一個(gè) Ajax 請求),由于無法控制執(zhí)行和完成的順序,所以就無法使用傳統(tǒng)的過程式寫法,函數(shù)式就會展現(xiàn)出其優(yōu)勢。
問題在于,傳統(tǒng)的函數(shù)式寫法實(shí)在太不友好了。
傳統(tǒng)寫法下,當(dāng)我們調(diào)用一個(gè) Ajax 時(shí),就要給它一個(gè)回調(diào)函數(shù),這樣當(dāng) Ajax 完成時(shí),就會調(diào)用它。當(dāng)邏輯簡單的時(shí)候,這毫無問題。但是我要串起 10 個(gè) Ajax 請求時(shí)該怎么辦呢?十重嵌套嗎?恩?似乎有點(diǎn)不對勁兒!
這就是回調(diào)地獄。
不僅如此,有時(shí)候我到底需要串起多少個(gè) Ajax 請求是未知的,要串起哪些也同樣是未知的。這已經(jīng)不再是地獄,而是《Mission: Impossible》了。
我,承諾(Promise),幫你解決
事實(shí)上,這樣的問題早在 1976 年就已經(jīng)被發(fā)現(xiàn)并解決了。注意,我沒寫錯,確實(shí)是 1976 年。
承諾,英文是 Promise [?prɑm?s],它的基本思想是借助一個(gè)代表回執(zhí)的變量來把回調(diào)地獄拍平。
我們以購物為例來看看日常生活中的承諾。
- 你去電商平臺下單,并付款
- 平臺會給你一個(gè)訂單號,這個(gè)訂單號本質(zhì)上是一個(gè)回執(zhí),代表商家做出了“稍后我將給你發(fā)貨”的承諾
- 商家發(fā)貨給你,在這個(gè)過程中你不用等待(異步)
- 過一段時(shí)間,快遞到了
- 你簽收(回調(diào)函數(shù)被調(diào)用)商品(回調(diào)參數(shù))
- 這次承諾結(jié)束
這是最直白的單步驟回調(diào),如果理解了它,再繼續(xù)往下看。
你跟電商下的單,但是卻從快遞(并不屬于商家)那里接收到了商品,仔細(xì)想想,你不覺得奇怪嗎?雖然表面看確實(shí)是商家給你的商品,但我們分解開中間步驟就會發(fā)現(xiàn)還有一些幕后的步驟。
- 商家把商品交給快遞公司,給快遞公司一個(gè)訂單號(老的回執(zhí))并拿回一個(gè)運(yùn)單號(新的回執(zhí))
- 快遞公司執(zhí)行這個(gè)新承諾,這個(gè)過程中商家不用等待(異步)
- 快遞公司完成這個(gè)新承諾,你收到這個(gè)新承諾攜帶的商品
所以,事實(shí)上,這個(gè)購物流程包括兩個(gè)承諾:
- 商家對你的一個(gè)發(fā)貨承諾
- 快遞公司對商家的運(yùn)貨承諾
因此,只要把這些承諾串起來,這些異步動作也就同樣串起來了。
當(dāng)我們把每個(gè)承諾都抽象成一個(gè)對象時(shí),我們就可以對任意數(shù)量、任意順序的承諾進(jìn)行組合,變成一個(gè)新的承諾。因此回調(diào)地獄不復(fù)存在,前述的 Mission 也變得 Possible 了。
Promise 的缺點(diǎn)
Promise 固然是一個(gè)重大的進(jìn)步,但在有些場景下仍然是不夠的。比如,Promise 的特點(diǎn)是無論有沒有人關(guān)心它的執(zhí)行結(jié)果,它都會立即開始執(zhí)行,并且你沒有機(jī)會取消這次執(zhí)行。顯然,在某些情況下這么做是浪費(fèi)的甚至錯誤的。仍然以電商為例,如果某商戶的訂單不允許取消,你還會去買嗎?再舉個(gè)編程領(lǐng)域的例子:如果你發(fā)起了一個(gè) Ajax 請求,然后用戶導(dǎo)航到了另一個(gè)路由,顯然,你這個(gè)請求如果還沒有完成就應(yīng)該被取消,而不應(yīng)該發(fā)出去。但是使用 Promise,你做不到,不是因?yàn)閷?shí)現(xiàn)方面的原因,而是因?yàn)樗诟拍顚樱ń涌诙x上)就無法支持取消。
此外,由于 Promise 只會承載一個(gè)值,因此當(dāng)我們要處理的是一個(gè)集合的時(shí)候就比較困難了。比如對于一個(gè)隨機(jī)數(shù)列(總數(shù)未知),如果我們要借助 Web API 檢查每個(gè)數(shù)字的有效性,然后對前一百個(gè)有效數(shù)字進(jìn)行求和,那么用 Promise 寫就比較麻煩了。
我們需要一個(gè)更高級的 Promise。
Observable
它就是可觀察對象(Observable [?b?z?rv?bl]),Observable 顧名思義就是可以被別人觀察的對象,當(dāng)它變化時(shí),觀察者就可以得到通知。換句話說,它負(fù)責(zé)生產(chǎn)數(shù)據(jù),別人可以消費(fèi)它生產(chǎn)的數(shù)據(jù)。
如果你是個(gè)資深后端,那么可能還記得 MessageQueue 的工作模式,它們很像。如果不懂 MQ 也沒關(guān)系,我還是用日常知識給你打個(gè)比方。
Observable 就像個(gè)傳送帶。這個(gè)傳送帶不斷運(yùn)行,圍繞這個(gè)傳送帶建立了一條生產(chǎn)線,包括一系列工序,不同的工序承擔(dān)單一而確定的職責(zé)。每個(gè)工位上有一個(gè)工人。
整個(gè)傳送帶的起點(diǎn)是原料箱,原料箱中的原料不斷被放到傳送帶上。工人只需要待在自己的工位上,對面前的原料進(jìn)行加工,然后放回傳送帶上或放到另一條傳送帶上即可,簡單、高效、無意外 —— 符合程序員的審美。
而且這個(gè)生產(chǎn)線還非常先進(jìn) —— 不接單就不生產(chǎn),非常有效地杜絕了浪費(fèi)。
FRP
這種設(shè)計(jì),看上去很美,對吧?但光看著漂亮可不行,在編程時(shí)要怎么實(shí)現(xiàn)呢?實(shí)際上,這是一種編程范式,叫做函數(shù)響應(yīng)式編程(FRP)。它比 Promise 可年輕多了,直到 1997 年才被人提出來。
顧名思義,F(xiàn)RP 同時(shí)具有函數(shù)式編程和響應(yīng)式編程的特點(diǎn)。響應(yīng)式編程是什么呢?形象的說,它的工作模式就是“飯來張口,衣來伸手”,也就是說,等待外界的輸入,并做出響應(yīng)。流水線每個(gè)工位上的工人正是這種工作模式。
工業(yè)上,流水線是人類管理經(jīng)驗(yàn)的結(jié)晶,它所做的事情是什么呢?本質(zhì)上就是把每個(gè)處理都局部化,以減小復(fù)雜度(降低對工人素質(zhì)的要求)。而這,正是軟件行業(yè)所求之不得的。響應(yīng)式,就是編程領(lǐng)域的流水線。
那么函數(shù)式呢?函數(shù)式最顯著的特征就是沒有副作用,而這恰好是對流水線上每個(gè)工序的要求。顯然,如果某個(gè)工序的操作會導(dǎo)致整個(gè)生產(chǎn)線平移 10 米,那么用不了多久這個(gè)生產(chǎn)線就要掉到海里了,這樣的生產(chǎn)線毫無價(jià)值。
因此,響應(yīng)式和函數(shù)式幾乎是注定要在一起的。
ReactiveX
2012 年,微軟 .NET 開發(fā)組的一個(gè)團(tuán)隊(duì)為了給 LinQ 設(shè)計(jì)擴(kuò)展機(jī)制而引入了 FRP 概念,卻發(fā)現(xiàn) FRP 的價(jià)值不止于此。于是一個(gè)新的項(xiàng)目出現(xiàn)了,它就是 ReactiveX。
嚴(yán)格來說 ReactiveX 應(yīng)該是一組 FRP 庫,因?yàn)樗鼛缀踉诿總€(gè)主流語言下都提供了實(shí)現(xiàn),而且這些實(shí)現(xiàn)都是語言原生風(fēng)格的,不是簡單地遷移。如果你在任何語言下用過帶有 Rx 前綴的庫,那多半兒就是 ReactiveX 的一個(gè)實(shí)現(xiàn)了,如 RxJava、Rx.NET、RxGroovy、RxSwift 等等。
ReactiveX 本身其實(shí)并不難,難的是 FRP 編程范式以及對操作符(operator)的理解。所以,只要學(xué)會了任何一個(gè) Rx* 庫,那么其它語言的庫就可以觸類旁通了。
寶石圖
為了幫助開發(fā)者更容易地理解 ReactiveX 的工作原理,ReactiveX 開發(fā)組還設(shè)計(jì)了一種很形象的圖,那就是寶石圖。這貨長這樣(英文注釋不必細(xì)看,接下來我會簡單解釋下):
中間的帶箭頭的線就像傳送帶,用來表示數(shù)據(jù)序列,這個(gè)數(shù)據(jù)序列被稱為“流”。上方的流叫做輸入流,下方的流叫做輸出流。輸入流可能有多個(gè),但是輸出流只會有一個(gè)(不過,流中的每個(gè)數(shù)據(jù)項(xiàng)也可以是別的流)。
數(shù)據(jù)序列上的每個(gè)圓圈表示一個(gè)數(shù)據(jù)項(xiàng),圓圈的位置表示數(shù)據(jù)出現(xiàn)的先后順序,但是一般不會表示精確的時(shí)間比例,比如在一毫秒內(nèi)接連出現(xiàn)的兩個(gè)數(shù)據(jù)之間仍然會有較大的距離。只有少數(shù)涉及到時(shí)間的操作,其寶石圖才會表現(xiàn)出精確的時(shí)間比例。
圓圈的最后,通常會有一條豎線或者一個(gè)叉號。豎線表示這個(gè)流正常終止了,也就是說不會再有更多的數(shù)據(jù)提供出來了。而叉號表示這個(gè)流拋出錯誤導(dǎo)致異常中止了。還有一種流,既沒有豎線也沒有叉號,這種叫做無盡流,比如一個(gè)由所有自然數(shù)組成的流就不會主動終止。但是要注意,無盡流仍然是可以處理的,因?yàn)樾枰嗌夙?xiàng)是由消費(fèi)者決定的。你可以把這個(gè)“智能”傳送帶理解為由下一個(gè)工位“叫號”的,沒“叫號”下一項(xiàng)數(shù)據(jù)就不會過來。
中間的大方框表示一個(gè)操作,也就是 operator —— 一個(gè)函數(shù),比如這個(gè)圖中的操作就是把輸入流中的條目乘以十后放入輸出流中。
看懂了寶石圖,就能很形象的理解各種操作符了。
RxJS
主角登場了。RxJS 就是 ReactiveX 在 JavaScript 語言上的實(shí)現(xiàn)。對于 JavaScript 程序員來說,不管你是前端還是 NodeJS 后端,RxJS 都會令你受益。
由于 JavaScript 本身的缺陷,RxJS 不得不采用了很多怪異的寫法。它對于 Java / C# 等背景的程序員來說可能會顯得比較怪異,不過,你可以先忽略它們,聚焦在編程范式和接下來要講的操作符語義上。
典型的寫法
of(1,2,3).pipe(
filter(item=>item % 2 === 1),
map(item=>item * 3),
).subscribe(item=> console.log(item))
`</pre>
它會輸出:
<pre>`3
9
其中 of 稱為創(chuàng)建器(creator),用來創(chuàng)建流,它返回一個(gè) Observable 類型的對象,filter 和 map 稱為操作符(operator),用來對條目進(jìn)行處理。這些操作符被當(dāng)作 Observable 對象的 pipe 方法的參數(shù)傳進(jìn)去。誠然,這個(gè)寫法略顯怪異,不過這主要是被 js 的設(shè)計(jì)缺陷所迫,它已經(jīng)是目前 js 體系下多種解決方案中相對好看的一種了。
Observable 對象的 subscribe 方法表示消費(fèi)者要訂閱這個(gè)流,當(dāng)流中出現(xiàn)數(shù)據(jù)時(shí),傳給 subscribe 方法的回調(diào)函數(shù)就會被調(diào)用,并且把這個(gè)數(shù)據(jù)傳進(jìn)去。這個(gè)回調(diào)函數(shù)可能被調(diào)用很多次,取決于這個(gè)流中有多少條數(shù)據(jù)。
注意,Observable 必須被 subscribe 之后才會開始生產(chǎn)數(shù)據(jù)。如果沒人 subscribe 它,那就什么都不會做。
簡單創(chuàng)建器
廣義上,創(chuàng)建器也是操作符的一種,不過這里我們把它單獨(dú)拿出來講。要啟動生產(chǎn)線,我們得先提供原料。本質(zhì)上,這個(gè)提供者就是一組函數(shù),當(dāng)流水線需要拿新的原料時(shí),就會調(diào)用它。
你當(dāng)然可以自己實(shí)現(xiàn)這個(gè)提供者,但通常是不用的。RxJS 提供了很多預(yù)定義的創(chuàng)建器,而且將來可能還會增加新的。不過,那些眼花繚亂的創(chuàng)建器完全沒必要全記住,只要記住少數(shù)幾個(gè)就夠了,其它的有時(shí)間慢慢看。
of - 單一值轉(zhuǎn)為流
它接收任意多個(gè)參數(shù),參數(shù)可以是任意類型,然后它會把這些參數(shù)逐個(gè)放入流中。
from - 數(shù)組轉(zhuǎn)為流
它接受一個(gè)數(shù)組型參數(shù),數(shù)組中可以有任意數(shù)據(jù),然后把數(shù)組的每個(gè)元素逐個(gè)放入流中。
range - 范圍轉(zhuǎn)為流
它接受兩個(gè)數(shù)字型參數(shù),一個(gè)起點(diǎn),一個(gè)終點(diǎn),然后按 1 遞增,把中間的每個(gè)數(shù)字(含邊界值)放入流中。
fromPromise - Promise 轉(zhuǎn)為流
接受一個(gè) Promise,當(dāng)這個(gè) Promise 有了輸出時(shí),就把這個(gè)輸出放入流中。
要注意的是,當(dāng) Promise 作為參數(shù)傳給 fromPromise 時(shí),這個(gè) Promise 就開始執(zhí)行了,你沒有機(jī)會防止它被執(zhí)行。
如果你需要這個(gè) Promise 被消費(fèi)時(shí)才執(zhí)行,那就要改用接下來要講的 defer 創(chuàng)建器。
defer - 惰性創(chuàng)建流
它的參數(shù)是一個(gè)用來生產(chǎn)流的工廠函數(shù)。也就是說,當(dāng)消費(fèi)方需要流(注意不是需要流中的值)的時(shí)候,就會調(diào)用這個(gè)函數(shù),創(chuàng)建一個(gè)流,并從這個(gè)流中進(jìn)行消費(fèi)(取數(shù)據(jù))。
因此,當(dāng)我們定義 defer 的時(shí)候,實(shí)際上還不存在一個(gè)真正的流,只是給出了創(chuàng)建這個(gè)流的方法,所以叫惰性創(chuàng)建流。
timer - 定時(shí)器流
它有兩個(gè)數(shù)字型的參數(shù),第一個(gè)是首次等待時(shí)間,第二個(gè)是重復(fù)間隔時(shí)間。從圖上可以看出,它實(shí)際上是個(gè)無盡流 —— 沒有終止線。因此它會按照預(yù)定的規(guī)則往流中不斷重復(fù)發(fā)出數(shù)據(jù)。
要注意,雖然名字有相關(guān)性,但它不是 setTimeout 的等價(jià)物,事實(shí)上它的行為更像是 setInterval。
interval - 定時(shí)器流
它和 timer 唯一的差別是它只接受一個(gè)參數(shù)。事實(shí)上,它就是一個(gè)語法糖,相當(dāng)于 timer(1000, 1000),也就是說初始等待時(shí)間和間隔時(shí)間是一樣的。
如果需求確實(shí)是 interval 的語義,那么就優(yōu)先使用這個(gè)語法糖,畢竟,從行為上它和 setInterval 幾乎是一樣的。
思考題:假設(shè)點(diǎn)了一個(gè)按鈕之后我要立刻開始一個(gè)動作,然后每隔 1000 毫秒重復(fù)一次,該怎么做?換句話說:該怎么移除首次延遲時(shí)間?
Subject - 主體對象
它和創(chuàng)建器不同,創(chuàng)建器是供直接調(diào)用的函數(shù),而 Subject 則是一個(gè)實(shí)現(xiàn)了 Observable 接口的類。也就是說,你要先把它 new 出來(假設(shè)實(shí)例叫 subject),然后你就可以通過程序控制的方式往流里手動放數(shù)據(jù)了。它的典型用法是用來管理事件,比如當(dāng)用戶點(diǎn)擊了某個(gè)按鈕時(shí),你希望發(fā)出一個(gè)事件,那么就可以調(diào)用 subject.next(someValue) 來把事件內(nèi)容放進(jìn)流中。
當(dāng)你希望手動控制往這個(gè)流中放數(shù)據(jù)的時(shí)機(jī)時(shí),這種特性非常有用。
當(dāng)然,Subject 其實(shí)并沒有這么簡單,用法也很多,不過這部分內(nèi)容超出了本文的范圍。
合并創(chuàng)建器
我們不但可以直接創(chuàng)建流,還可以對多個(gè)現(xiàn)有的流進(jìn)行不同形式的合并,創(chuàng)建一個(gè)新的流。常見的合并方式有三種:并聯(lián)、串聯(lián)、拉鏈。
merge - 并聯(lián)
從圖上我們可以看到兩個(gè)流中的內(nèi)容被合并到了一個(gè)流中。只要任何一個(gè)流中出現(xiàn)了值就會立刻被輸出,哪怕其中一個(gè)流是完全空的也不影響結(jié)果 —— 等同于原始流。
這種工作方式非常像電路中的并聯(lián)行為,因此我稱其為并聯(lián)創(chuàng)建器。
并聯(lián)在什么情況下起作用呢?舉個(gè)例子吧:有一個(gè)列表需要每隔 5 秒鐘定時(shí)刷新一次,但是一旦用戶按了搜索按鈕,就必須立即刷新,而不能等待 5 秒間隔。這時(shí)候就可以用一個(gè)定時(shí)器流和一個(gè)自定義的用戶操作流(subject)merge 在一起。這樣,無論哪個(gè)流中出現(xiàn)了數(shù)據(jù),都會進(jìn)行刷新。
concat - 串聯(lián)
從圖中我們可以看到兩個(gè)流中的內(nèi)容被按照順序放進(jìn)了輸出流中。前面的流尚未結(jié)束時(shí)(注意豎線),后面的流就會一直等待。
這種工作方式非常像電路中的串聯(lián)行為,因此我稱其為串聯(lián)創(chuàng)建器。
串聯(lián)的適用場景就很容易想象了,比如我們需要先通過 Web API 進(jìn)行登錄,然后取學(xué)生名冊。這兩個(gè)操作就是異步且串聯(lián)工作的。
zip - 拉鏈
zip 的直譯就是拉鏈,事實(shí)上,有些壓縮軟件的圖標(biāo)就是一個(gè)帶拉鏈的鑰匙包。拉鏈的特點(diǎn)是兩邊各有一個(gè)“齒”,兩者會嚙合在一起。這里的 zip 操作也是如此。
從圖上我們可以看到,兩個(gè)輸入流中分別出現(xiàn)了一些數(shù)據(jù),當(dāng)僅僅輸入流 A 中出現(xiàn)了數(shù)據(jù)時(shí),輸出流中什么都沒有,因?yàn)樗€在等另一個(gè)“齒”。當(dāng)輸出流 B 中出現(xiàn)了數(shù)據(jù)時(shí),兩個(gè)“齒”都湊齊了,于是對這兩個(gè)齒執(zhí)行中間定義的運(yùn)算(取 A 的形狀,B 的顏色,并合成為輸出數(shù)據(jù))。
可以看到,當(dāng)任何一個(gè)流先行結(jié)束之后,整個(gè)輸出流也就結(jié)束了。
拉鏈創(chuàng)建器適用的場景要少一些,通常用于合并兩個(gè)數(shù)據(jù)有對應(yīng)關(guān)系的數(shù)據(jù)源。比如一個(gè)流中是姓名,另一個(gè)流中是成績,還有一個(gè)流中是年齡,如果這三個(gè)流中的每個(gè)條目都有精確的對應(yīng)關(guān)系,那么就可以通過 zip 把它們合并成一個(gè)由表示學(xué)生成績的對象組成的流。
操作符
RxJS 有很多操作符,事實(shí)上比創(chuàng)建器還要多一些,但是我們并不需要一一講解,因?yàn)樗鼈冎械暮艽笠徊糠侄际呛瘮?shù)式編程中的標(biāo)配,比如 map、reduce、filter 等。有 Java 8 / scala / kotlin 等基礎(chǔ)的后端或者用過 underscore/lodash 的前端都可以非常容易地理解它們。
本文重點(diǎn)講解一些傳統(tǒng)方式下沒有的或不常用的:
retry - 失敗時(shí)重試
有些錯誤是可以通過重試進(jìn)行恢復(fù)的,比如臨時(shí)性的網(wǎng)絡(luò)丟包。甚至一些流程的設(shè)計(jì)還會故意借助重試機(jī)制,比如當(dāng)你發(fā)起請求時(shí),如果后端發(fā)現(xiàn)你沒有登錄過,就會給你一個(gè) 401 錯誤,然后你可以完成登錄并重新開始整個(gè)流程。
retry 操作符就是負(fù)責(zé)在失敗時(shí)自動發(fā)起重試的,它可以接受一個(gè)參數(shù),用來指定最大重試次數(shù)。
這里我為什么一直在強(qiáng)調(diào)失敗時(shí)重試呢?因?yàn)檫€有一個(gè)操作符負(fù)責(zé)成功時(shí)重試。
repeat - 成功時(shí)重試
除了重復(fù)的條件之外,repeat 的行為幾乎和 retry 一模一樣。
repeat 很少會單獨(dú)用,一般會組合上 delay 操作,以提供暫停時(shí)間,否則就容易 DoS 了服務(wù)器。
delay - 延遲
這才是真正的 setTimeout 的等價(jià)操作。它接受一個(gè)毫秒數(shù)(圖中是 20 毫秒),每當(dāng)它從輸入流中讀取一個(gè)數(shù)據(jù)之后,會先等待 20 毫秒,然后再放到輸出流中。
可以看到,輸入流和輸出流內(nèi)容是完全一樣的,只是時(shí)機(jī)上,輸出流中的每個(gè)條目都恰好比輸入流晚 20 毫秒出現(xiàn)。
toArray - 收集為數(shù)組
事實(shí)上,你幾乎可以把它看做是 from 的逆運(yùn)算。 from 把數(shù)組打散了逐個(gè)放進(jìn)流中,而 toArray 恰好相反,把流中的內(nèi)容收集到一個(gè)數(shù)組中 —— 直到這個(gè)流結(jié)束。
這個(gè)操作符幾乎總是放在最后一步,因?yàn)?RxJS 的各種 operator 本身就可以對流中的數(shù)據(jù)進(jìn)行很多類似數(shù)組的操作,比如查找最小值、最大值、過濾等。所以通常會先使用各種 operator 對數(shù)據(jù)流進(jìn)行處理,等到要脫離 RxJS 的體系時(shí),再轉(zhuǎn)換成數(shù)組傳出去。
debounceTime - 防抖
在 underscore/lodash 中這是常用函數(shù)。 所謂防抖其實(shí)就是“等它平靜下來”。比如預(yù)輸入(type ahead)功能,當(dāng)用戶正在快速打字的時(shí)候,你沒必要立刻去查服務(wù)器,否則可能直接讓服務(wù)器掛了,而應(yīng)該等用戶稍作停頓(平靜下來)時(shí)再發(fā)起查詢。
debounceTime 就是這樣,你傳入一個(gè)最小平靜時(shí)間,在這個(gè)時(shí)間窗口內(nèi)連續(xù)過來的數(shù)據(jù)一概被忽略,一旦平靜時(shí)間超過它,就會往把接收到的下一條數(shù)據(jù)放到流中。這樣消費(fèi)者就只能看到平靜時(shí)間超時(shí)之后發(fā)來的最后一條數(shù)據(jù)。
switchMap - 切換成另一個(gè)流
這可能是相對較難理解的一個(gè) operator。
有時(shí)候,我們會希望根據(jù)一個(gè)立即數(shù)發(fā)起一個(gè)遠(yuǎn)程查詢,并且把這個(gè)異步取回的結(jié)果放進(jìn)流中。比如,流中是一些學(xué)生的 id,每過來一個(gè) id,你要發(fā)起一個(gè) Ajax 請求來根據(jù)這個(gè) id 獲取這個(gè)學(xué)生的詳情,并且把詳情放進(jìn)輸出流中。
注意,這是一個(gè)異步操作,所以你沒法用普通的 map 來實(shí)現(xiàn),否則映射出來的結(jié)果就會是一個(gè)個(gè) Observable 對象。
switchMap 就是用來解決這個(gè)問題的。它在回調(diào)函數(shù)中接受從輸入流中傳來的數(shù)據(jù),并轉(zhuǎn)換成一個(gè)新的 Observable 對象(新的流,每個(gè)流中包括三個(gè)值,每個(gè)值都等于輸入值的十倍),switchMap 會訂閱這個(gè) Observable 對象,并把它的值放入輸出流中。注意圖中豎線的位置 —— 只有當(dāng)所有新的流都結(jié)束時(shí),輸出流才會結(jié)束。
不知道你有沒有注意到這里一個(gè)很重要的細(xì)節(jié)。30 只生成了兩個(gè)值,而不是我們所預(yù)期的三個(gè)。這是因?yàn)楫?dāng)輸入流中的 5 到來時(shí),會切換到以 5 為參數(shù)構(gòu)建出的這個(gè)新流(S5),而這時(shí)候基于 3 構(gòu)建的那個(gè)流(S3)尚未結(jié)束。雖然如此,但是已經(jīng)沒人再訂閱 S3 了,因?yàn)橥粫r(shí)刻 switchMap 只能訂閱一個(gè)流。所以,已經(jīng)沒人會再朝著 S3 “叫號”了,它已經(jīng)被釋放了。
規(guī)律:operator 打包學(xué)
當(dāng)你掌握了一些基本操作符之后,就可以讓自己的操作符知識翻倍了。
這是因?yàn)?RxJS 中的很多操作符都遵循著同樣的命名模式。比如:
xxxWhen - 滿足條件時(shí) xxx
它接受一個(gè) Observable 型參數(shù)作為條件流,一旦這個(gè)條件流中出現(xiàn)任意數(shù)據(jù),則進(jìn)行 xxx 操作。
如 retryWhen(notifier$),其中的 notifier$ 就是一個(gè)條件流。當(dāng)輸入流出現(xiàn)異常時(shí),就會開始等待 notifier$ 流中出現(xiàn)數(shù)據(jù),一旦出現(xiàn)了任何數(shù)據(jù)(不管是什么值),就會開始執(zhí)行重試邏輯。
xxxCount - 拿到 n 個(gè)數(shù)據(jù)項(xiàng)時(shí) xxx
它接受一個(gè)數(shù)字型參數(shù)作為閾值,一旦從輸入流中取到了 n 個(gè)數(shù)據(jù),則進(jìn)行 xxx 操作。
如 bufferCount(3) 表示每拿到 3 個(gè)數(shù)據(jù)就進(jìn)行一次 buffer 操作。
這個(gè)操作可以看做是 xxxWhen 的語法糖。
xxxTime - 超時(shí)后 xxx
它接受一個(gè)超時(shí)時(shí)間作為參數(shù),從輸入流中取數(shù)據(jù),一旦到達(dá)超時(shí)時(shí)間,則執(zhí)行 xxx 操作。
比如前面講過的 debounceTime 其實(shí)遵循的就是這種模式。
這個(gè)操作可以看做 xxxWhen 的語法糖。
xxxTo - 用立即量代替 Lambda 表達(dá)式
它接受一個(gè)立即量作為參數(shù),相當(dāng)于 xxx(()=&gt;value))。
比如 mapTo('a') 其實(shí)是 map(()=&gt;'a') 的語法糖,也就是說無論輸入流中給出的值是什么,我往輸出流中放入的都是這個(gè)固定的值。
坑與最佳實(shí)踐
取消訂閱
subscribe 之后,你的回調(diào)函數(shù)就被別人引用了,因此如果不撤銷對這個(gè)回調(diào)函數(shù)的引用,那么與它相關(guān)的內(nèi)存就永遠(yuǎn)不會釋放,同時(shí),它仍然會在流中有數(shù)據(jù)過來時(shí)被調(diào)用,可能會導(dǎo)致奇怪的 console.log 等意外行為。
因此,必須找到某個(gè)時(shí)機(jī)撤銷對這個(gè)回調(diào)函數(shù)的引用。但其實(shí)不一定需要那么麻煩。解除對回調(diào)函數(shù)的引用有兩種時(shí)機(jī),一種是這個(gè)流完成(complete,包括正常結(jié)束和異常結(jié)束)了,一種是訂閱方主動取消。當(dāng)流完成時(shí),會自動解除全部訂閱回調(diào),而所有的有限流都是會自動完成的。只有無盡流才需要特別處理,也就是訂閱方要主動取消訂閱。
當(dāng)調(diào)用 Observable 的 subscribe 方法時(shí),會返回一個(gè) Subscription 類型的引用,它實(shí)際上是一個(gè)訂閱憑證。把它保存下來,等恰當(dāng)?shù)臅r(shí)機(jī)調(diào)用它的 unsubscribe 方法就可以取消訂閱了。比如在 Angular 中,如果你訂閱了無盡流,那么就需要把訂閱憑證保存在私有變量里,并且在 ngOnDestroy 回調(diào)中調(diào)用它的 unsubscribe 方法。
類型檢查
只要有可能,請盡量使用 TypeScript 來書寫 RxJS 程序。由于大量 operator 都會改變流中的數(shù)據(jù)類型,因此如果靠人力來追蹤數(shù)據(jù)類型的變化既繁瑣又容易出錯。TypeScript 的類型檢查可以給你提供很大的幫助,既省心又安全,而且這兩個(gè)都是微軟家的,搭配使用,風(fēng)味更佳。
代碼風(fēng)格
如同所有 FP 程序一樣,ReactiveX 的代碼也應(yīng)該由一系列小的、單一職責(zé)的、無副作用的函數(shù)組成。雖然 JavaScript 無法像 Java 中那樣對 Lambda 表達(dá)式的副作用做出編譯期限制,但是仍然要遵循同樣的原則,堅(jiān)持無副作用和數(shù)據(jù)不變性。
寄語 - 實(shí)踐出真知
ReactiveX 大家族看似龐大,實(shí)則簡單 —— 如果你已經(jīng)有了 Java 8+ / Kotlin / underscore 或 lodash 等函數(shù)式基礎(chǔ)知識時(shí),新東西就很少了。而當(dāng)你用過 Rx 大家族中的任何一個(gè)成員時(shí),RxJS 對你幾乎是免費(fèi)的,反之也一樣。
唯一的問題,就是找機(jī)會實(shí)踐,并體會 FRP 風(fēng)格的獨(dú)特之處,獲得那些超乎具體技術(shù)之上的真知灼見。
文/ThoughtWorks 汪志成















