ReactiveCocoa學(xué)習(xí)筆記(三):響應(yīng)式和函數(shù)響應(yīng)式編程

上一篇談了談我自己對函數(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),比如下面這段代碼:


可以看到腊尚,我們的每段代碼在時間順序上都是一段彼此獨立的的時間范圍吨拗。即使是異步的操作,比如各種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值……

響應(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)用中對于我們來說并不重要,ReactiveCocoaGithub主頁也介紹自己為「Streams of values over time」塞俱。重要的是能夠理解函數(shù)響應(yīng)式編程的思想姐帚,這樣,在使用類似框架的時候障涯,才能做到知其然并知其所以然卧土。


Reference

Why Reactive(Cocoa)?

The introduction to Reactive Programming you've been missing

stackoverflow - What is functional reactive programming

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市像樊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌旅敷,老刑警劉巖生棍,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異媳谁,居然都是意外死亡涂滴,警方通過查閱死者的電腦和手機褪猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門一疯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饲常,你說我怎么就攤上這事锤躁「榱希” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵系羞,是天一觀的道長郭计。 經(jīng)常有香客問我,道長椒振,這世上最難降的妖魔是什么昭伸? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮澎迎,結(jié)果婚禮上庐杨,老公的妹妹穿的比我還像新娘选调。我一直安慰自己,他們只是感情好灵份,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布仁堪。 她就那樣靜靜地躺著,像睡著了一般各吨。 火紅的嫁衣襯著肌膚如雪枝笨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天揭蜒,我揣著相機與錄音横浑,去河邊找鬼。 笑死屉更,一個胖子當(dāng)著我的面吹牛徙融,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瑰谜,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼欺冀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了萨脑?” 一聲冷哼從身側(cè)響起隐轩,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渤早,沒想到半個月后职车,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡鹊杖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年悴灵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骂蓖。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡积瞒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出登下,到底是詐尸還是另有隱情茫孔,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布被芳,位于F島的核電站银酬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏筐钟。R本人自食惡果不足惜揩瞪,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望篓冲。 院中可真熱鬧李破,春花似錦宠哄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妇菱,卻和暖如春承粤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闯团。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工辛臊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人房交。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓彻舰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親候味。 傳聞我的和親對象是個殘疾皇子刃唤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容