響應式編程(Reactive Programming)介紹
很明顯你是有興趣學習這種被稱作響應式編程的新技術纔來看這篇文章的。
學習響應式編程是很困難的一個過程,特別是在缺乏優秀資料的前提下。剛開始學習時,我試過去找一些教程,並找到了爲數不多的實用教程,但是它們都流於表面,從沒有圍繞響應式編程構建起一個完整的知識體系。庫的文檔往往也無法幫助你去了解它的函數。不信的話可以看一下這個:
通過合併元素的指針,將每一個可觀察的元素序列放射到一個新的可觀察的序列中,然後將多個可觀察的序列中的一個轉換成一個只從最近的可觀察序列中產生值得可觀察的序列。
天啊。
我看過兩本書,一本只是講述了一些概念,而另一本則糾結於如何使用響應式編程庫。我最終放棄了這種痛苦的學習方式,決定在開發中一邊使用響應式編程,一邊理解它。在 Wikipedia 一如既往的空泛與理論化。Reactive Manifesto 看起來是你展示給你公司的項目經理或者老闆們看的東西。微軟的 Observer Design Pattern。
上面的示意圖也可以使用ASCII重畫爲下圖,在下面的部分教程中我們會使用這幅圖:
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
既然已經開始對響應式編程感到熟悉,爲了不讓你覺得無聊,我們可以嘗試做一些新東西:我們將會把一個 Click event stream 轉爲新的 Click event stream。
首先,讓我們做一個能記錄一個按鈕點擊了多少次的計數器 Stream。在常見的響應式編程庫中,每個Stream都會有多個方法,如 map
, filter
, scan
,
等等。當你調用其中一個方法時,例如 clickStream.map(f)
,它就會基於原來的 Click stream 返回一個新的
Stream 。它不會對原來的 Click steam 作任何修改。這個特性稱爲不可變性,它對於響應式編程 Stream,就如果汁對於薄煎餅。我們也可以對方法進行鏈式調用,如 clickStream.map(f).scan(g)
:
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f)
會根據你提供的 f
函數把原
Stream 中的 Value 分別映射到新的 Stream 中。在我們的例子中,我們把每一次 Click 都映射爲數字 1。scan(g)
會根據你提供的 g
函數把
Stream 中的所有 Value 聚合成一個 Value x = g(accumulated, current)
,這個示例中 g
只是一個簡單的添加函數。然後,每
Click 一次, counterStream
就會把點擊的總次數發給它的觀察者。
爲了展示響應式編程真正的實力,讓我們假設你想得到一個包含“雙擊”事件的 Stream。爲了讓它更加有趣,假設我們想要的這個 Stream 要同時考慮三擊(Triple clicks),或者更加寬泛,連擊(兩次或更多)。深呼吸一下,然後想像一下在傳統的命令式且帶狀態的方式中你會怎麼實現。我敢打賭代碼會像一堆亂麻,並且會使用一些變量保存狀態,同時也有一些計算時間間隔的代碼。
而在響應式編程中,這個功能的實現就非常簡單。事實上,這邏輯只有 RxJS 作爲工具 ,因爲JavaScript是現在最多人會的語言,而 .NET, JavaScript, Python,Objective-C/Cocoa, Groovy等等)。所以,無論你用的是什麼工具,你都能從下面這個教程中受益。
實現"Who to follow"推薦界面
在 Twitter 上,這個表明其他賬戶的 UI 元素看起來是這樣的:
我們將會重點模擬它的核心功能,如下:
- 啓動時從 API 那裏加載帳戶數據,並顯示 3 個推薦
- 點擊"Refresh"時,加載另外 3 個推薦用戶到這三行中
- 點擊帳戶所在行的'x'按鈕時,只清除那一個推薦然後顯示一個新的推薦
- 每行都會顯示帳戶的頭像,以及他們主頁的鏈接
我們可以忽略其它的特性和按鈕,因爲它們是次要的。同時,因爲 Twitter 最近關閉了對非授權用戶的 API,我們將會爲 Github 實現這個推薦界面,而非 Twitter。這是subscribing 這個 Stream。
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}
留意一下我們使用了 jQuery 的 Ajax 函數(我們假設你已經知道 Rx.Observable.create()
所做的事就是通過顯式的通知每一個
Observer (或者說是“Subscriber”) Data events(onNext()
)或者 Errors ( onError()
)來創建你自己的
Stream。而我們所做的就只是把 jQuery Ajax Promise 包裝起來而已。打擾一下,這意味者Promise本質上就是一個Observable?
是的。
Observable 就是 Promise++。在 Rx 中,你可以用 var stream = Rx.Observable.fromPromise(promise)
輕易的把一個
Promise 轉爲 Observable,所以我們就這樣子做吧。唯一的不同就是 Observable 並不遵循pointers:每個映射的值都是一個指向其它
Stream 的指針。在我們的例子裏,每個請求 URL 都會被映射一個指向包含響應 Promise stream 的指針。
Response 的 Metastream 看起來會讓人困惑,並且看起來也沒有幫到我們什麼。我們只想要一個簡單的響應 stream,其中每個映射的值應該是 JSON 對象,而不是一個 JSON 對象的'Promise'。是時候介紹 (Mr. Flatmap)(merge()
函數。這就是它做的事的圖解:
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
這樣就簡單了:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
var requestStream = Rx.Observable.merge(
requestOnRefreshStream, startupRequestStream
);
還有一個更加簡潔的可選方案,不需要使用中間變量。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.merge(Rx.Observable.just('https://api.github.com/users'));
甚至可以更簡短,更具有可讀性:
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.startWith('https://api.github.com/users');
<a rel="nofollow" href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md" "#rxobservableprototypestartwithscheduler-args="" style="box-sizing: border-box; color: rgb(45, 133, 202); text-decoration: none; background-color: transparent;">startWith()
函數做的事和你預期的完全一樣。無論你輸入的
Stream 是怎樣,startWith(x)
輸出的 Stream 一開始都是 x 。但是還不夠 Separation
of concerns。還記得響應式編程的咒語麼?
所以讓我們把顯示的推薦設計成一個 stream,其中每一個映射的值都是包含了推薦內容的 JSON 對象。我們以此把三個推薦內容分開來。現在第一個推薦看起來是這樣子的:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
});
其他的, suggestion2Stream
和 suggestion3Stream
可以簡單的拷貝 suggestion1Stream
的代碼來使用。這不是
DRY,它會讓我們的例子變得更加簡單一些,加之我覺得這是一個可以幫助考慮如何減少重複的良好實踐。
我們不在 responseStream 的 subscribe() 中處理渲染了,我們這麼處理:
suggestion1Stream.subscribe(function(suggestion) {
// render the 1st suggestion to the DOM
});
回到"當刷新時,清理掉當前的推薦",我們可以很簡單的把刷新點擊映射爲 null
,並且在 suggestion1Stream
中包含進來,如下:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
);
當渲染時,null
解釋爲"沒有數據",所以把 UI 元素隱藏起來。
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});
現在的示意圖:
refreshClickStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestion1Stream: ----s-----N---s----N-s-->
suggestion2Stream: ----q-----N---q----N-q-->
suggestion3Stream: ----t-----N---t----N-t-->
其中,N
代表了 null
作爲一種補充,我們也可以在一開始的時候就渲染“空的”推薦內容。這通過把 startWith(null)
添加到 Suggestion stream 就完成了:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
現在結果是:
refreshClickStream: ----------o---------o---->
requestStream: -r--------r---------r---->
responseStream: ----R----------R------R-->
suggestion1Stream: -N--s-----N----s----N-s-->
suggestion2Stream: -N--q-----N----q----N-q-->
suggestion3Stream: -N--t-----N----t----N-t-->
關閉推薦並使用緩存的響應
還有一個功能需要實現。每一個推薦,都該有自己的"X"按鈕以關閉它,然後在該位置加載另一個推薦。最初的想法,點擊任何關閉按鈕時都需要發起一個新的請求:
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button
var requestStream = refreshClickStream.startWith('startup click')
.merge(close1ClickStream) // we added this
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
這個沒有效果。這將會關閉並且重新加載 所有 的推薦,而不是僅僅處理我們點擊的那一個。有一些不一樣的方法可以解決,並且讓它變得更加有趣,我們可以通過複用之前的請求來解決它。API 的響應頁面有 100 個用戶,而我們僅僅使用其中的三個,所以還有很多的新數據可以使用,無須重新發起請求。
同樣的,我們用Stream的方式來思考。當點擊'close1'時,我們想要用 responseStream
最近的映射從響應列表中獲取一個隨機的用戶,如:
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
在 Rx* 中, 叫做連接符函數的 big list of functions,它包括瞭如何轉換、合併、以及創建 Observable。如果你想通過圖表去理解這些函數,請看 Cold vs Hot Observables 中的概念。如果忽略了這些,你一不小心就會被它坑了。我提醒過你了。通過學習真正的函數式編程去提升自己的技能,並熟悉那些會影響到 Rx 的問題,比如副作用。
但是響應式編程不僅僅是 Rx。還有相對容易理解的 Elm Language 則以它自己的方式支持 RP:它是一門會編譯成 Javascript + HTML + CSS 的響應式編程語言 ,並有一個 RxJava 是實現Netflix's API服務器端併發的一個重要組件 。Rx 並不是一個只能在某種應用或者語言中使用的 Framework。它本質上是一個在開發任何 Event-driven 軟件中都能使用的編程範式。
本文來自於:http://wiki.jikexueyuan.com/project/android-weekly/issue-145/introduction-to-RP.html