上一篇談了談我自己對函數(shù)式編程的理解蝉揍。這篇文章會講到恭取,響應(yīng)式編程钞瀑,函數(shù)響應(yīng)式編程這些又是個啥,以及我們?yōu)槭裁匆褂盟鼈儭?/p>
響應(yīng)式編程
對于響應(yīng)式編程,我沒有找到比這篇文章更為生動詳盡的文章了汰寓,因此這里大部分是翻譯自原文吆寨,加上了一些我自己的思考。
輸入和輸出
本質(zhì)上來說踩寇,我們構(gòu)建應(yīng)用時,都是在做一件事情:等待一些事件的發(fā)生六水,來提供一些信息作為輸入俺孙。我們根據(jù)這些輸入的信息,進行某些處理掷贾,生成特定的結(jié)果并輸出睛榄。
輸入可以是多種多樣的:「用戶點擊了一個按鈕」是一種輸入;「服務(wù)器有數(shù)據(jù)返回了」是一種輸入想帅;某個方法的回調(diào)是一種輸入场靴;或者某個對象的某個屬性的變化也可以是一種輸入「圩迹看著很眼熟哈旨剥,我們每天都在和這些輸入打交道:
///
// delegate
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}
// block based callbacks
[TCAPI getCategoriesOnComplete:^(NSArray *objects, HTTPOperation *operation, NSError *error) {
}];
// target action
- (IBAction)buttonAction:(id)sender {
}
// timers
[NSTimer scheduledTimerWithTimeInterval:.1 target:self
selector:@selector(spinIt:) userInfo:nil repeats:YES];
// KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
}
這些都是Objective-C提供的通信機制,來給我們傳達一些輸入信息:某個事件發(fā)生了浅缸,怎么處理你看著辦吧轨帜。(題外話:objc.io的這篇文章對于這些通信機制講的很好)
而我們的輸出也是各種各樣的:我們可以將一些信息保存在本地;或者我們可以通過網(wǎng)絡(luò)協(xié)議將一些信息保存在服務(wù)器上衩椒;當(dāng)然蚌父,對于移動端app來說,我們最主要的輸出毛萌,還是根據(jù)情況去更新UI界面苟弛,以便展示新的信息給用戶。
線性編程(Linear Programming)
然而阁将,麻煩的事情來了:我們的每個輸出膏秫,幾乎不會只和一個輸入相關(guān):當(dāng)收到一個用戶點擊事件的輸入時,我們需要更新一個UI界面冀痕,但是界面的更新往往也依賴于服務(wù)器的數(shù)據(jù)返回荔睹,或是之前用戶的其他操作;更麻煩的是言蛇,我們的輸入和輸出是異步的:我們的輸出和輸入在時間順序上是分離的僻他。
造成這個麻煩的原因是:我們傳統(tǒng)的編程處理輸入和輸出的方式是線性的(Linear Programming)
,比如下面這段代碼:
![](http://www.sprynthesis.com/assets/images/code-timeline.png)
可以看到腊尚,我們的每段代碼在時間順序上都是一段彼此獨立的的時間范圍吨拗。即使是異步的操作,比如各種
callback
,也不過是讓我們在時間軸中間插入一段代碼去執(zhí)行劝篷。(這里沒有說到多線程哨鸭。然而即使是多線程,也只是在另一個線程上開辟了一條新的時間軸娇妓,開始一段新的線性編程的故事……)
現(xiàn)在讓我們來看看上面說的麻煩事兒吧——當(dāng)我們接收到某個輸入事件的時候像鸡,我們往往需要作出相應(yīng)的輸出,這個輸出往往不僅取決于當(dāng)前的輸入哈恰,而且是和時間軸上處于前面的輸入造成的結(jié)果相關(guān)的:
哎呀用戶點擊這個「菜單」按鈕了只估,這個時候應(yīng)當(dāng)展示出下拉菜單了。但是着绷,首先要確認的是蛔钙,用戶當(dāng)前是否已經(jīng)登錄了呢?這個菜單所需要的數(shù)據(jù)是否已經(jīng)從服務(wù)器拿到了呢荠医?用戶之前點過這個按鈕了么吁脱?現(xiàn)在是應(yīng)當(dāng)展示這個菜單還是收起這個菜單呢?……
相信我們對這樣的邏輯也是再熟悉不過了彬向,我們每天都在這樣寫代碼(手動滑稽)兼贡。我們的程序像個傻瓜金魚,只有7秒鐘的記憶幢泼。這樣說太夸張了紧显,其實我們的程序連1毫秒的記憶也沒有:)。我們只好在有輸入到來的時候缕棵,回過頭再去check一遍之前時間軸上發(fā)生的事情孵班,檢查一些必要的信息,然后做出輸出招驴。
好篙程,回憶一下我們是怎么去做到的呢?用什么去追蹤時間軸上前面發(fā)生的故事的呢别厘?我們也很無奈啊虱饿,我們只好引入了一個又一個的「狀態(tài)」。
狀態(tài)(State)
什么是「狀態(tài)」触趴?「狀態(tài)」是程序運行中的參數(shù)的記錄氮发,是程序“現(xiàn)在長啥樣”的描述。
@property (nonatomic, assign) BOOL userIsLoggedIn;
@property (nonatomic, assign) BOOL menuDataIsLoaded;
@property (nonatomic, assign) BOOL isMenuShowing;
...
這些是為了解決上面那個惱人的問題所需要記錄的狀態(tài)冗懦。當(dāng)時間軸上有事件發(fā)生的時候爽冕,我們?nèi)ジ逻@些狀態(tài);如果某個輸出需要用到這些信息披蕉,我們再去檢查這些property
當(dāng)前的值颈畸。我們手動去追蹤程序的狀態(tài)乌奇,在各個必要的地方去更新它們,然后在一個名為xxxUpdate
的方法中眯娱,寫一些復(fù)雜的判斷邏輯來根據(jù)這些狀態(tài)給出我們的輸出:
// a central function that checks all our states and generates the appropriate output
- (void) checkAndUpdateMenuStatus {
if (self.menuShouldBeShowing && !self.isMenuShowing
&& self.menuDataIsLoaded && self.userIsLoggedIn) {
[self showMenu];
} else if (!self.menuShouldBeShowing && self.isMenuShowing) {
[self hideMenu];
}
}
// sets initial states and sets up our notification observation
- (void) viewDidLoad {
// Set initial states
// Let's assume you can't get to this page without being logged in
self.userIsLoggedIn = YES;
self.isMenuShowing = NO;
self.menuDataIsLoaded = NO;
self.menuShouldBeShowing = NO;
// Need to handle in case the user logs out while on this page
[[NSNotificationCenter defaultCenter] addObserverForName:kUserLoggedOutNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
self.userIsLoggedIn = NO;
[self checkAndUpdateMenuStatus];
}];
// set the initial state (somewhat unnecessary since our menu starts hidden
// but a good safety check)
[self checkAndUpdateMenuStatus];
}
// Loads the menu data from the network
- (void) loadMenuData {
[TCPAPI fetchUserMenuData onComplete:^(NSArray *objects, NSError *error) {
[self hideLoadingView];
if(!error) {
self.menuDataIsLoaded = YES;
[self checkAndUpdateMenuStatus];
}
}];
}
// handles showing and hiding a loading view
- (void) startLoadingView {
if(self.isLoadingShowing) return;
self.isLoadingShowing = YES;
// do work to show loading view
}
- (void) hideLoadingView {
if(!self.isLoadingShowing) return;
self.isLoadingShowing = NO;
// do work to hide loading view
}
- (void) showMenu {
// show menu
self.isMenuShowing = YES;
}
- (void) hideMenu {
// hide menu
self.isMenuShowing = NO;
}
- (IBAction) userTappedMenuButton:(UIButton *menuButton) {
// kick off loading of our menu data lazily if it isn't loaded yet
if(!self.menuDataIsLoaded) {
[self loadMenuData];
}
self.menuShouldBeShowing = !self.menuShouldBeShowing;
[self checkAndUpdateMenuStatus];
}
(心累……原文作者你平時是在看著我寫程序嗎礁苗,還是說天下程序猿都是一樣的傻的可愛)
上面列舉的僅僅是更新一個UI所需要的狀態(tài)。糟糕的是徙缴,當(dāng)我們需要新的信息的時候试伙,我們往往會不假思索地再添上一個property
,畢竟已經(jīng)形成了肌肉記憶了于样。慢慢地迁霎,我們的代碼里充滿了這些property
,以及狀態(tài)判斷的if...else...
邏輯百宇。如果有一個地方出了bug,我們得慢慢去找秘豹,哪個環(huán)節(jié)讓我們親愛的狀態(tài)出了問題携御。更可怕的是,狀態(tài)帶來的復(fù)雜度是隨著狀態(tài)數(shù)量增加呈指數(shù)級增長的——上面3種狀態(tài)便能帶來2^3種情況既绕,前提還是這3種狀態(tài)都是一個BOOL
值……
![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1505585974098&di=14500d713bca3617e3177ca4088eecb1&imgtype=jpg&src=http://img0.imgtn.bdimg.com/it/u=3109653125,4251890918&fm=214&gp=0.jpg)
響應(yīng)式編程(Reactive Programming)
既然狀態(tài)這么不好啄刹,那我們可不可以不要它了呢?聰明的猿們想到了種種方法凄贩,讓計算機來幫我追蹤和記錄這些狀態(tài)誓军。而我們的工作是在時間軸的開始,就向計算機解釋清楚:我需要哪些輸入信息才能做出一個特定的輸出疲扎,對于一些輸入昵时,我需要做出什么輸出,剩下的事情椒丧,就交給計算機去做啦壹甥。
最常見的例子就是我們的「AutoLayout」:我們向計算機說道:“嗯,這個頁面放在這個頁面里壶熏,它的高度是父頁面的一半句柠,上邊距為10dp,左右居中展示”棒假。然后就哦啦溯职。計算機會在父頁面的大小和布局發(fā)生改變的時候,幫我們?nèi)フ{(diào)整子頁面的大小和位置帽哑,而不需要我們在各個地方手動去寫一堆setFrame:
方法谜酒。
這就是響應(yīng)式編程(Reactive Programming)
:我們代碼里,只是說明了各個事件(輸入)的關(guān)系祝拯,以及它們相應(yīng)的輸出甚带。當(dāng)這些事件(輸入)發(fā)生的時候她肯,計算機根據(jù)我們的說明,去進行恰當(dāng)?shù)捻憫?yīng)鹰贵∏绨保「狀態(tài)」依然是存在的,只不過我們將它們托付給了計算機去處理碉输。響應(yīng)式編程
處理了時間軸上輸入和輸出的異步問題籽前,讓我們輕裝上陣,對付各種各樣的業(yè)務(wù)邏輯敷钾。
移動app時代枝哄,隨著UI元素越來越多,用戶交互越來越復(fù)雜阻荒,處理越來越頻繁挠锥,需要的實時性也越來越高,這也是響應(yīng)式編程
越來越受到開發(fā)者們的青睞的原因吧侨赡。
函數(shù)響應(yīng)式編程
響應(yīng)式編程
給我們帶來了許多的好處蓖租,Cocoa框架中也為我們提供了不少響應(yīng)式編程
的支持,例如Autolayout
羊壹,KVO
等等蓖宦。但是,有沒有可能更進一步呢油猫?
上一篇文章講到稠茂,函數(shù)式編程中,可以將「數(shù)據(jù)」和「副作用」等封裝成一個monad
情妖,然后就可以盡享函數(shù)式編程的鏈?zhǔn)骄幊痰慕z滑體驗了睬关。那如果將我們響應(yīng)式編程
中的「輸入」和處理它們的「異步」的邏輯,抽象成一個monad
呢毡证?那么共螺,我們將可以使用鏈?zhǔn)秸Z法和各種強大的函數(shù)式編程的工具,處理各種「輸入」情竹,以及讓「輸入」在函數(shù)式的“管道”中經(jīng)過一步步地處理藐不,最終成為我們需要的「輸出」。
沒錯秦效,這就是函數(shù)響應(yīng)式
編程的魅力了雏蛮!它將一個隨時間變化的值抽象成一個流,并通過monad
使其可以利用到函數(shù)式編程的強大工具阱州,最終讓我們可以方便直觀地處理各種「輸入」和「輸出」的異步處理邏輯挑秉。
Functional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter). -- Wikipedia
函數(shù)響應(yīng)式編程&函數(shù)式響應(yīng)式編程
網(wǎng)絡(luò)上的教程都說,ReactiveCocoa
(以及RXSwift
)是一個函數(shù)式響應(yīng)式
的編程框架苔货,而沒有說是“函數(shù)響應(yīng)式
框架”犀概,讓人傻傻分不清楚立哑。這是為什么呢?
在上文中姻灶,其實強調(diào)了函數(shù)響應(yīng)式
中抽象出的值流是隨時間連續(xù)變化的铛绰,其抽象稱為「behaviors」;而像ReactiveCocoa
(或是RXSwift
)這類框架是應(yīng)用于主要處理人機交互的移動軟件的产喉,它抽象出的「輸入」流是時間軸上離散的一個個事件捂掰,稱為「Event」。這就是兩者的區(qū)別所在曾沈。
其實我個人覺得这嚣,這種區(qū)分在實際的應(yīng)用中對于我們來說并不重要,ReactiveCocoa
的Github主頁也介紹自己為「Streams of values over time」塞俱。重要的是能夠理解函數(shù)響應(yīng)式
編程的思想姐帚,這樣,在使用類似框架的時候障涯,才能做到知其然并知其所以然卧土。
Reference
The introduction to Reactive Programming you've been missing