美文网首页
ReactiveCocoa学习笔记(三):响应式和函数响应式编程

ReactiveCocoa学习笔记(三):响应式和函数响应式编程

作者: 阳仔dynamics | 来源:发表于2017-10-30 23:24 被阅读186次

    上一篇谈了谈我自己对函数式编程的理解。这篇文章会讲到,响应式编程,函数响应式编程这些又是个啥,以及我们为什么要使用它们。

    响应式编程

    对于响应式编程,我没有找到比这篇文章更为生动详尽的文章了,因此这里大部分是翻译自原文,加上了一些我自己的思考。

    输入和输出

    本质上来说,我们构建应用时,都是在做一件事情:等待一些事件的发生,来提供一些信息作为输入。我们根据这些输入的信息,进行某些处理,生成特定的结果并输出。

    输入可以是多种多样的:「用户点击了一个按钮」是一种输入;「服务器有数据返回了」是一种输入;某个方法的回调是一种输入;或者某个对象的某个属性的变化也可以是一种输入。看着很眼熟哈,我们每天都在和这些输入打交道:

    ///
    // 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提供的通信机制,来给我们传达一些输入信息:某个事件发生了,怎么处理你看着办吧。(题外话:objc.io的这篇文章对于这些通信机制讲的很好)

    而我们的输出也是各种各样的:我们可以将一些信息保存在本地;或者我们可以通过网络协议将一些信息保存在服务器上;当然,对于移动端app来说,我们最主要的输出,还是根据情况去更新UI界面,以便展示新的信息给用户。

    线性编程(Linear Programming)

    然而,麻烦的事情来了:我们的每个输出,几乎不会只和一个输入相关:当收到一个用户点击事件的输入时,我们需要更新一个UI界面,但是界面的更新往往也依赖于服务器的数据返回,或是之前用户的其他操作;更麻烦的是,我们的输入和输出是异步的:我们的输出和输入在时间顺序上是分离的。

    造成这个麻烦的原因是:我们传统的编程处理输入和输出的方式是线性的(Linear Programming),比如下面这段代码:


    好,回忆一下我们是怎么去做到的呢?用什么去追踪时间轴上前面发生的故事的呢?我们也很无奈啊,我们只好引入了一个又一个的「状态」

    状态(State)

    什么是「状态」?「状态」是程序运行中的参数的记录,是程序“现在长啥样”的描述。

    @property (nonatomic, assign) BOOL userIsLoggedIn;
    @property (nonatomic, assign) BOOL menuDataIsLoaded;
    @property (nonatomic, assign) BOOL isMenuShowing;
    ...
    

    这些是为了解决上面那个恼人的问题所需要记录的状态。当时间轴上有事件发生的时候,我们去更新这些状态;如果某个输出需要用到这些信息,我们再去检查这些property当前的值。我们手动去追踪程序的状态,在各个必要的地方去更新它们,然后在一个名为xxxUpdate的方法中,写一些复杂的判断逻辑来根据这些状态给出我们的输出:

    // 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所需要的状态。糟糕的是,当我们需要新的信息的时候,我们往往会不假思索地再添上一个property,毕竟已经形成了肌肉记忆了。慢慢地,我们的代码里充满了这些property,以及状态判断的if...else...逻辑。如果有一个地方出了bug,我们得慢慢去找,哪个环节让我们亲爱的状态出了问题。更可怕的是,状态带来的复杂度是随着状态数量增加呈指数级增长的——上面3种状态便能带来2^3种情况,前提还是这3种状态都是一个BOOL值……

    响应式编程(Reactive Programming)

    既然状态这么不好,那我们可不可以不要它了呢?聪明的猿们想到了种种方法,让计算机来帮我追踪和记录这些状态。而我们的工作是在时间轴的开始,就向计算机解释清楚:我需要哪些输入信息才能做出一个特定的输出,对于一些输入,我需要做出什么输出,剩下的事情,就交给计算机去做啦。

    最常见的例子就是我们的「AutoLayout」:我们向计算机说道:“嗯,这个页面放在这个页面里,它的高度是父页面的一半,上边距为10dp,左右居中展示”。然后就哦啦。计算机会在父页面的大小和布局发生改变的时候,帮我们去调整子页面的大小和位置,而不需要我们在各个地方手动去写一堆setFrame:方法。

    这就是响应式编程(Reactive Programming):我们代码里,只是说明了各个事件(输入)的关系,以及它们相应的输出。当这些事件(输入)发生的时候,计算机根据我们的说明,去进行恰当的响应。「状态」依然是存在的,只不过我们将它们托付给了计算机去处理。响应式编程处理了时间轴上输入和输出的异步问题,让我们轻装上阵,对付各种各样的业务逻辑。

    移动app时代,随着UI元素越来越多,用户交互越来越复杂,处理越来越频繁,需要的实时性也越来越高,这也是响应式编程越来越受到开发者们的青睐的原因吧。

    函数响应式编程

    响应式编程给我们带来了许多的好处,Cocoa框架中也为我们提供了不少响应式编程的支持,例如AutolayoutKVO等等。但是,有没有可能更进一步呢?

    上一篇文章讲到,函数式编程中,可以将「数据」和「副作用」等封装成一个monad,然后就可以尽享函数式编程的链式编程的丝滑体验了。那如果将我们响应式编程中的「输入」和处理它们的「异步」的逻辑,抽象成一个monad呢?那么,我们将可以使用链式语法和各种强大的函数式编程的工具,处理各种「输入」,以及让「输入」在函数式的“管道”中经过一步步地处理,最终成为我们需要的「输出」。

    没错,这就是函数响应式编程的魅力了!它将一个随时间变化的值抽象成一个流,并通过monad使其可以利用到函数式编程的强大工具,最终让我们可以方便直观地处理各种「输入」和「输出」的异步处理逻辑。

    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

    函数响应式编程&函数式响应式编程

    网络上的教程都说,ReactiveCocoa(以及RXSwift)是一个函数式响应式的编程框架,而没有说是“函数响应式框架”,让人傻傻分不清楚。这是为什么呢?

    在上文中,其实强调了函数响应式中抽象出的值流是随时间连续变化的,其抽象称为「behaviors」;而像ReactiveCocoa(或是RXSwift)这类框架是应用于主要处理人机交互的移动软件的,它抽象出的「输入」流是时间轴上离散的一个个事件,称为「Event」。这就是两者的区别所在。

    其实我个人觉得,这种区分在实际的应用中对于我们来说并不重要,ReactiveCocoaGithub主页也介绍自己为「Streams of values over time」。重要的是能够理解函数响应式编程的思想,这样,在使用类似框架的时候,才能做到知其然并知其所以然。


    Reference

    Why Reactive(Cocoa)?

    The introduction to Reactive Programming you've been missing

    stackoverflow - What is functional reactive programming

    相关文章

      网友评论

          本文标题:ReactiveCocoa学习笔记(三):响应式和函数响应式编程

          本文链接:https://www.haomeiwen.com/subject/znhvpxtx.html