精华内容
下载资源
问答
  • 函数响应式编程

    千次阅读 2016-08-18 14:04:29
    原文链接 : The introduction to Reactive Programming you've been missing作者 : @...相信你们在学习响应式编程这个新技术的时候都会充满了好奇,特别是它的一些变体,例如:Rx系列、Bacon.js、RAC等等……
     
    

    相信你们在学习响应式编程这个新技术的时候都会充满了好奇,特别是它的一些变体,例如:Rx系列、Bacon.js、RAC等等……

    在缺乏优秀资料的前提下,响应式编程的学习过程将满是荆棘。起初,我试图寻找一些教程,却只找到少量的实践指南,而且它们讲的都非常浅显,从来没人接受围绕响应式编程建立一个完整知识体系的挑战。此外,官方文档通常也不能很好地帮助你理解某些函数,因为它们通常看起来很绕,不信请看这里:

    Rx.Observable.prototype.flatMapLatest(selector, [thisArg])

    根据元素下标,将可观察序列中每个元素一一映射到一个新的可观察序列当中,然后...%…………%&¥#@@……&**(晕了)

    天呐,这简直太绕了!

    我读过两本相关的书,一本只是在给你描绘响应式编程的伟大景象,而另一本却只是深入到如何使用响应式库而已。我在不断的构建项目过程中把响应式编程了解的透彻了一些,最后以这种艰难的方式学完了响应式编程。在我工作公司的一个实际项目中我会用到它,当我遇到问题时,还可以得到同事的支持。

    学习过程中最难的部分是如何以响应式的方式来思考,更多的意味着要摒弃那些老旧的命令式和状态式的典型编程习惯,并且强迫自己的大脑以不同的范式来运作。我还没有在网络上找到任何一个教程是从这个层面来剖析的,我觉得这个世界非常值得拥有一个优秀的实践教程来教你如何以响应式编程的方式来思考,方便引导你开始学习响应式编程。然后看各种库文档才可以给你更多的指引。希望这篇文章能够帮助你快速地进入响应式编程的世界。

    "什是响应式编程?"

    网络上有一大堆糟糕的解释和定义,如Wikipedia上通常都是些非常笼统和理论性的解释,而Stackoverflow上的一些规范的回答显然也不适合新手来参考,Reactive Manifesto看起来也只像是拿给你的PM或者老板看的东西,微软的Rx术语"Rx = Observables + LINQ + Schedulers" 也显得太过沉重,而且充满了太多微软式的东西,反而给我们带来更多疑惑。相对于你使用的MV*框架以及你钟爱的编程语言,"Reactive"和"Propagation of change"这样的术语并没有传达任何有意义的概念。当然,我的view框架能够从model做出反应,我的改变当然也会传播,如果没有这些,我的界面根本就没有东西可渲染。

    所以,不要再扯这些废话了。

    响应式编程就是与异步数据流交互的编程范式

    一方面,这已经不是什么新事物了。事件总线(Event Buses)或一些典型的点击事件本质上就是一个异步事件流(asynchronous event stream),这样你就可以观察它的变化并使其做出一些反应(do some side effects)。响应式是这样的一个思路:除了点击和悬停(hover)的事件,你还可以给其他任何事物创建数据流。数据流无处不在,任何东西都可以成为一个数据流,例如变量、用户输入、属性、缓存、数据结构等等。举个栗子,你可以把你的微博订阅功能想象成跟点击事件一样的数据流,你可以监听这样的数据流,并做出相应的反应。

    最重要的是,你会拥有一些令人惊艳的函数去结合、创建和过滤任何一组数据流。 这就是"函数式编程"的魔力所在。一个数据流可以作为另一个数据流的输入,甚至多个数据流也可以作为另一个数据流的输入。你可以合并两个数据流,也可以过滤一个数据流得到另一个只包含你感兴趣的事件的数据流,还可以映射一个数据流的值到一个新的数据流里。

    数据流是整个响应式编程体系中的核心,要想学习响应式编程,当然要先走进数据流一探究竟了。那现在就让我们先从熟悉的"点击一个按钮"的事件流开始

    Click event stream

    一个数据流是一个按时间排序的即将发生的事件(Ongoing events ordered in time)的序列。如上图,它可以发出3种不同的事件(上一句已经把它们叫做事件):一个某种类型的值事件,一个错误事件和一个完成事件。当一个完成事件发生时,在某些情况下,我们可能会做这样的操作:关闭包含那个按钮的窗口或者视图组件。

    我们只能异步捕捉被发出的事件,使得我们可以在发出一个值事件时执行一个函数,发出错误事件时执行一个函数,发出完成事件时执行另一个函数。有时候你可以忽略后两个事件,只需聚焦于如何定义和设计在发出值事件时要执行的函数,监听这个事件流的过程叫做订阅,我们定义的函数叫做观察者,而事件流就可以叫做被观察的主题(或者叫被观察者)。你应该察觉到了,对的,它就是观察者模式

    上面的示意图我们也可以用ASCII码的形式重新画一遍,请注意,下面的部分教程中我们会继续使用这幅图:

    --a---b-c---d---X---|->
    
    a, b, c, d 是值事件
    X 是错误事件
    | 是完成事件
    ---> 是时间线(轴)
    

    现在你对响应式编程事件流应该非常熟悉了,为了不让你感到无聊,让我们来做一些新的尝试吧:我们将创建一个由原始点击事件流演变而来的一种新的点击事件流。

    首先,让我们来创建一个记录按钮点击次数的事件流。在常用的响应式库中,每个事件流都会附有一些函数,例如 map,filterscan等,当你调用这其中的一个方法时,比如clickStream.map(f),它会返回基于点击事件流的一个新事件流。它不会对原来的点击事件流做任何的修改。这种特性叫做不可变性(immutability),而且它可以和响应式事件流搭配在一起使用,就像豆浆和油条一样完美的搭配。这样我们可以用链式函数的方式来调用,例如: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函数把原事件流中每一个返回值分别映射到新的事件流中。在上图的例子中,我们把每一次点击事件都映射成数字1,scan(g)函数则把之前映射的值聚集起来,然后根据x = g(accumulated, current)算法来作相应的处理,而本例的g函数其实就是简单的加法函数。然后,当一个点击事件发生时,counterStream函数则上报当前点击事件总数。

    为了展示响应式编程真正的魅力,我们假设你有一个"双击"事件流,为了让它更有趣,我们假设这个事件流同时处理"三次点击"或者"多次点击"事件,然后深吸一口气想想如何用传统的命令式和状态式的方式来处理,我敢打赌,这么做会相当的讨厌,其中还要涉及到一些变量来保存状态,并且还得做一些时间间隔的调整。

    而用响应式编程的方式处理会非常的简洁,实际上,逻辑处理部分只需要四行代码。但是,当前阶段让我们现忽略代码的部分,无论你是新手还是专家,看着图表思考来理解和建立事件流将是一个非常棒的方法。

    多次点击事件流

    图中,灰色盒子表示将上面的事件流转换下面的事件流的函数过程,首先根据250毫秒的间隔时间(event silence, 译者注:无事件发生的时间段,上一个事件发生到下一个事件发生的间隔时间)把点击事件流一段一隔开,再将每一段的一个或多个点击事件添加到列表中(这就是这个函数:buffer(stream.throttle(250ms))所做的事情,当前我们先不要急着去理解细节,我们只需专注响应式的部分先)。现在我们得到的是多个含有事件流的列表,然后我们使用了map()中的函数来算出每一个列表长度的整数数值映射到下一个事件流当中。最后我们使用了过滤filter(x >= 2) 函数忽略掉了小于1 的整数。就这样,我们用了3步操作生成了我们想要的事件流,接下来,我们就可以订阅("监听")这个事件并作出我们想要的操作了。

    我希望你能感受到这个示例的优雅之处。当然了,这个示例也只是响应式编程魔力的冰山一角而已,你同样可以将这3步操作应用到不同种类的事件流中去,例如,一串API响应的事件流。另一方面,你还有非常多的函数可以使用。

    "我为什么要采用响应式编程?"

    响应式编程可以加深你代码抽象的程度,让你可以更专注于定义与事件相互依赖的业务逻辑,而不是把大量精力放在实现细节上,同时,使用响应式编程还能让你的代码变得更加简洁。

    特别对于现在流行的webapps和mobile apps,它们的 UI 事件与数据频繁地产生交互,在开发这些应用时使用响应式编程的优点将更加明显。十年前,web页面的交互是通过提交一个很长的表单数据到后端,然后再做一些简单的前端渲染操作。而现在的Apps则演变的更具有实时性:仅仅修改一个单独的表单域就能自动的触发保存到后端的代码,就像某个用户对一些内容点了赞,就能够实时反映到其他已连接的用户一样,等等。

    当今的Apps都含有丰富的实时事件来保证一个高效的用户体验,我们就需要采用一个合适的工具来处理,那么响应式编程就正好是我们想要的答案。

    以响应式编程方式思考的例子

    让我们深入到一些真实的例子,一个能够一步一步教你如何以响应式编程的方式思考的例子,没有虚构的示例,没有一知半解的概念。在这个教程的末尾我们将产生一些真实的函数代码,并能够知晓每一步为什么那样做的原因(知其然,知其所以然)。

    我选了JavaScriptRxJS来作为本教程的编程语言,原因是:JavaScript是目前最多人熟悉的语言,而Rx系列的库对于很多语言和平台的运用是非常广泛的,例如(.NETJavaScalaClojureJavaScriptRubyPythonC++Objective-C/CocoaGroovy等等。所以,无论你用的是什么语言、库、工具,你都能从下面这个教程中学到东西(从中受益)。

    实现一个推荐关注(Who to follow)的功能

    在Twitter里有一个UI元素向你推荐你可以关注的用户,如下图:

    Twitter Who to follow suggestions box

    我们将聚焦于模仿它的主要功能,它们是:

    • 开始阶段,从API加载推荐关注的用户账户数据,然后显示三个推荐用户
    • 点击刷新,加载另外三个推荐用户到当前的三行中显示
    • 点击每一行的推荐用户上的'x'按钮,清楚当前被点击的用户,并显示新的一个用户到当前行
    • 每一行显示一个用户的头像并且在点击之后可以链接到他们的主页。

    我们可以先不管其他的功能和按钮,因为它们是次要的。因为Twitter最近关闭了未经授权的公共API调用,我们将用Github获取用户的API代替,并且以此来构建我们的UI。

    如果你想先看一下最终效果,这里有完成后的代码

    Request和Response

    在Rx中是怎么处理这个问题呢?,在开始之前,我们要明白,(几乎)一切都可以成为一个事件流,这就是Rx的准则(mantra)。让我们从最简单的功能开始:"开始阶段,从API加载推荐关注的用户账户数据,然后显示三个推荐用户"。其实这个功能没什么特殊的,简单的步骤分为: (1)发出一个请求,(2)获取响应数据,(3)渲染响应数据。ok,让我们把请求作为一个事件流,一开始你可能会觉得这样做有些夸张,但别急,我们也得从最基本的开始,不是吗?

    开始时我们只需做一次请求,如果我们把它作为一个数据流的话,它只能成为一个仅仅返回一个值的事件流而已。一会儿我们还会有很多请求要做,但当前,只有一个。

    --a------|->
    
    a就是字符串:'https://api.github.com/users'
    

    这是一个我们要请求的URL事件流。每当发生一个请求时,它将告诉我们两件事:什么时候做了什么事(when and what)。什么时候请求被执行,什么时候事件就被发出。而做了什么就是请求了什么,也就是请求的URL字符串。

    在Rx中,创建返回一个值的事件流是非常简单的。其实事件流在Rx里的术语是叫"被观察者",也就是说它是可以被观察的,但是我发现这名字比较傻,所以我更喜欢把它叫做事件流

    var requestStream = Rx.Observable.just('https://api.github.com/users');

    但现在,这只是一个字符串的事件流而已,并没有做其他操作,所以我们需要在发出这个值的时候做一些我们要做的操作,可以通过订阅(subscribing)这个事件来实现。

    requestStream.subscribe(function(requestUrl) {
      // execute the request
      jQuery.getJSON(requestUrl, function(responseData) {
        // ...
      });
    }

    注意到我们这里使用的是JQuery的AJAX回调方法(我们假设你已经很了解JQuery和AJAX了)来的处理这个异步的请求操作。但是,请稍等一下,Rx就是用来处理异步数据流的,难道它就不能处理来自请求(request)在未来某个时间响应(response)的数据流吗?好吧,理论上是可以的,让我们尝试一下。

    requestStream.subscribe(function(requestUrl) {
      // execute the request
      var responseStream = Rx.Observable.create(function (observer) {
        jQuery.getJSON(requestUrl)
        .done(function(response) { observer.onNext(response); })
        .fail(function(jqXHR, status, error) { observer.onError(error); })
        .always(function() { observer.onCompleted(); });
      });
    
      responseStream.subscribe(function(response) {
        // do something with the response
      });
    }

    Rx.Observable.create()操作就是在创建自己定制的事件流,且对于数据事件(onNext())和错误事件(onError())都会显示的通知该事件每一个观察者(或订阅者)。我们做的只是小小的封装一下jQuery Ajax Promise而已。等等,这是否意味者jQuery Ajax Promise本质上就是一个被观察者呢(Observable)?

    Amazed

    是的。

    Promise++就是被观察者(Observable),在Rx里你可以使用这样的操作:var stream = Rx.Observable.fromPromise(promise),就可以很轻松的将Promise转换成一个被观察者(Observable),非常简单的操作就能让我们现在就开始使用它。不同的是,这些被观察者都不能兼容Promises/A+,但理论上并不冲突。一个Promise就是一个只有一个返回值的简单的被观察者,而Rx就远超于Promise,它允许多个值返回。

    这样更好,这样更突出被观察者至少比Promise强大,所以如果你相信Promise宣传的东西,那么也请留意一下响应式编程能胜任些什么。

    现在回到示例当中,你应该能快速发现,我们在subscribe()方法的内部再次调用了subscribe()方法,这有点类似于回调地狱(callback hell),而且responseStream的创建也是依赖于requestStream的。在之前我们说过,在Rx里,有很多很简单的机制来从其他事件流的转化并创建出一些新的事件流,那么,我们也应该这样做试试。

    现在你需要了解的一个最基本的函数是map(f),它可以从事件流A中取出每一个值,并对每一个值执行f()函数,然后将产生的新值填充到事件流B。如果将它应用到我们的请求和响应事件流当中,那我们就可以将请求的URL映射到一个响应Promises上了(伪装成数据流)。

    var responseMetastream = requestStream
      .map(function(requestUrl) {
        return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
      });

    然后,我们创造了一个叫做"metastream"的怪兽:一个装载了事件流的事件流。先别惊慌,metastream就是每一个发出的值都是另一个事件流的事件流,你看把它想象成一个[指针(pointers)]((https://en.wikipedia.org/wiki/Pointer_(computer_programming))数组:每一个单独发出的值就是一个_指针_,它指向另一个事件流。在我们的示例里,每一个请求URL都映射到一个指向包含响应数据的promise数据流。

    Response metastream

    一个响应的metastream,看起来确实让人容易困惑,看样子对我们一点帮助也没有。我们只想要一个简单的响应数据流,每一个发出的值是一个简单的JSON对象就行,而不是一个'Promise' 的JSON对象。ok,让我们来见识一下另一个函数:Flatmap,它是map()函数的另一个版本,它比metastream更扁平。一切在"主躯干"事件流发出的事件都将在"分支"事件流中发出。Flatmap并不是metastreams的修复版,metastreams也不是一个bug。它俩在Rx中都是处理异步响应事件的好工具、好帮手。

    var responseStream = requestStream
      .flatMap(function(requestUrl) {
        return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
      });

    Response stream

    很赞,因为我们的响应事件流是根据请求事件流定义的,如果我们以后有更多事件发生在请求事件流的话,我们也将会在相应的响应事件流收到响应事件,就如所期待的那样:

    requestStream:  --a-----b--c------------|->
    responseStream: -----A--------B-----C---|->
    
    (小写的是请求事件流, 大写的是响应事件流)
    

    现在,我们终于有响应的事件流了,并且可以用我们收到的数据来渲染了:

    responseStream.subscribe(function(response) {
      // render `response` to the DOM however you wish
    });

    让我们把所有代码合起来,看一下:

    var requestStream = Rx.Observable.just('https://api.github.com/users');
    
    var responseStream = requestStream
      .flatMap(function(requestUrl) {
        return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
      });
    
    responseStream.subscribe(function(response) {
      // render `response` to the DOM however you wish
    });

    刷新按钮

    我还没提到本次响应的JSON数据是含有100个用户数据的list,这个API只允许指定页面偏移量(page offset),而不能指定每页大小(page size),我们只用到了3个用户数据而浪费了其他97个,现在可以先忽略这个问题,稍后我们将学习如何缓存响应的数据。

    每当刷新按钮被点击,请求事件流就会发出一个新的URL值,这样我们就可以获取新的响应数据。这里我们需要两个东西:点击刷新按钮的事件流(准则:一切都能作为事件流),我们需要将点击刷新按钮的事件流作为请求事件流的依赖(即点击刷新事件流会引起请求事件流)。幸运的是,RxJS已经有了可以从事件监听者转换成被观察者的方法了。

    var refreshButton = document.querySelector('.refresh');
    var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

    因为刷新按钮点击事件不会携带将要请求的API的URL,我们需要将每次的点击映射到一个实际的URL上,现在我们将请求事件流转换成了一个点击事件流,并将每次的点击映射成一个随机的页面偏移量(offset)参数来组成API的URL。

    var requestStream = refreshClickStream
      .map(function() {
        var randomOffset = Math.floor(Math.random()*500);
        return 'https://api.github.com/users?since=' + randomOffset;
      });

    因为我比较笨而且也没有使用自动化测试,所以我刚把之前做好的一个功能搞烂了。这样,请求在一开始的时候就不会执行,而只有在点击事件发生时才会执行。我们需要的是两种情况都要执行:刚开始打开网页和点击刷新按钮都会执行的请求。

    我们知道如何为每一种情况做一个单独的事件流:

    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');

    但是我们是否可以将这两个合并成一个呢?没错,是可以的,我们可以使用merge()方法来实现。下图可以解释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');

    startWith()函数做的事和你预期的完全一样。无论你的输入事件流是怎样的,使用startWith(x)函数处理过后输出的事件流一定是一个x 开头的结果。但是我没有总是重复代码( DRY),我只是在重复API的URL字符串,改进的方法是将 startWith()函数挪到refreshClickStream那里,这样就可以在启动时,模拟一个刷新按钮的点击事件了。

    var requestStream = refreshClickStream.startWith('startup click')
      .map(function() {
        var randomOffset = Math.floor(Math.random()*500);
        return 'https://api.github.com/users?since=' + randomOffset;
      });

    不错,如果你倒回到"搞烂了的自动测试"的地方,然后再对比这两个地方,你会发现我仅仅是加了一个startWith()函数而已。

    用事件流将3个推荐的用户数据模型化

    直到现在,在响应事件流(responseStream)的订阅(subscribe())函数发生的渲染步骤里,我们只是稍微提及了一下推荐关注的UI。现在有了刷新按钮,我们就会出现一个问题:当你点击了刷新按钮,当前的三个推荐关注用户没有被清楚,而只要响应的数据达到后我们就拿到了新的推荐关注的用户数据,为了让UI看起来更漂亮,我们需要在点击刷新按钮的事件发生的时候清楚当前的三个推荐关注的用户。

    refreshClickStream.subscribe(function() {
      // clear the 3 suggestion DOM elements 
    });

    不,老兄,还没那么快。我们又出现了新的问题,因为我们现在有两个订阅者在影响着推荐关注的UI DOM元素(另一个是responseStream.subscribe()),这看起来并不符合关注分离(Separation of concerns)原则,还记得响应式编程的原则么?

    Mantra

    现在,让我们把推荐关注的用户数据模型化成事件流形式,每个被发出的值是一个包含了推荐关注用户数据的JSON对象。我们将把这三个用户数据分开处理,下面是推荐关注的1号用户数据的事件流:

    var suggestion1Stream = responseStream
      .map(function(listUsers) {
        // get one random user from the list
        return listUsers[Math.floor(Math.random()*listUsers.length)];
      });

    其他的,如推荐关注的2号用户数据的事件流suggestion2Stream和推荐关注的3号用户数据的事件流suggestion3Stream 都可以方便的从suggestion1Stream 复制粘贴就好。这里并不是重复代码,只是为让我们的示例更加简单,而且我认为这是一个思考如何避免重复代码的好案例。

    Instead of having the rendering happen in responseStream's subscribe(), we do that here:

    suggestion1Stream.subscribe(function(suggestion) {
      // render the 1st suggestion to the DOM
    });

    我们不在responseStream的subscribe()中处理渲染了,我们这样处理:

    suggestion1Stream.subscribe(function(suggestion) {
      // render the 1st suggestion to the DOM
    });

    回到"当刷新时,清楚掉当前的推荐关注的用户",我们可以很简单的把刷新点击映射为没有推荐数据(null suggestion data),并且在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)添加到推荐关注的事件流就可以了:

    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-->
    

    推荐关注的关闭和使用已缓存的响应数据(responses)

    只剩这一个功能没有实现了,每个推荐关注的用户UI会有一个'x'按钮来关闭自己,然后在当前的用户数据UI中加载另一个推荐关注的用户。最初的想法是:点击任何关闭按钮时都需要发起一个新的请求:

    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个用户数据,而我们只使用了其中三个,所以还有一大堆未使用的数据可以拿来用,不用去请求更多数据了。

    ok,再来,我们继续用事件流的方式来思考。当'close1'点击事件发生时,我们想要使用最近发出的响应数据,并执行responseStream函数来从响应列表里随机的抽出一个用户数据来,就像下面这样:

        requestStream: --r--------------->
       responseStream: ------R----------->
    close1ClickStream: ------------c----->
    suggestion1Stream: ------s-----s----->
    

    在Rx中一个组合函数叫做combineLatest,应该是我们需要的。这个函数会把数据流A和数据流B作为输入,并且无论哪一个数据流发出一个值了,combineLatest 函数就会将从两个数据流最近发出的值ab作为f函数的输入,计算后返回一个输出值(c = f(x,y)),下面的图表会让这个函数的过程看起来会更加清晰:

    stream A: --a-----------e--------i-------->
    stream B: -----b----c--------d-------q---->
              vvvvvvvv combineLatest(f) vvvvvvv
              ----AB---AC--EC---ED--ID--IQ---->
    
    f是转换成大写的函数
    

    这样,我们就可以把combineLatest()函数用在close1ClickStream和 responseStream上了,只要关闭按钮被点击,我们就可以获得最近的响应数据,并在suggestion1Stream上产生出一个新值。另一方面,combineLatest()函数也是相对的:每当在responseStream上发出一个新的响应,它将会结合一次新的点击关闭按钮事件来产生一个新的推荐关注的用户数据,这非常有趣,因为它可以给我们的suggestion1Stream简化代码:

    var suggestion1Stream = close1ClickStream
      .combineLatest(responseStream,             
        function(click, listUsers) {
          return listUsers[Math.floor(Math.random()*listUsers.length)];
        }
      )
      .merge(
        refreshClickStream.map(function(){ return null; })
      )
      .startWith(null);

    现在,我们的拼图还缺一小块地方。combineLatest()函数使用了最近的两个数据源,但是如果某一个数据源还没有发出任何东西,combineLatest()函数就不能在输出流上产生一个数据事件。如果你看了上面的ASCII图表(文章中第一个图表),你会明白当第一个数据流发出一个值a时并没有任何的输出,只有当第二个数据流发出一个值b的时候才会产生一个输出值。

    这里有很多种方法来解决这个问题,我们使用最简单的一种,也就是在启动的时候模拟'close 1'的点击事件:

    var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
      .combineLatest(responseStream,             
        function(click, listUsers) {l
          return listUsers[Math.floor(Math.random()*listUsers.length)];
        }
      )
      .merge(
        refreshClickStream.map(function(){ return null; })
      )
      .startWith(null);

    封装起来

    我们完成了,下面是封装好的完整示例代码:

    var refreshButton = document.querySelector('.refresh');
    var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
    
    var closeButton1 = document.querySelector('.close1');
    var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
    // and the same logic for close2 and close3
    
    var requestStream = refreshClickStream.startWith('startup click')
      .map(function() {
        var randomOffset = Math.floor(Math.random()*500);
        return 'https://api.github.com/users?since=' + randomOffset;
      });
    
    var responseStream = requestStream
      .flatMap(function (requestUrl) {
        return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
      });
    
    var suggestion1Stream = close1ClickStream.startWith('startup click')
      .combineLatest(responseStream,             
        function(click, listUsers) {
          return listUsers[Math.floor(Math.random()*listUsers.length)];
        }
      )
      .merge(
        refreshClickStream.map(function(){ return null; })
      )
      .startWith(null);
    // and the same logic for suggestion2Stream and suggestion3Stream
    
    suggestion1Stream.subscribe(function(suggestion) {
      if (suggestion === null) {
        // hide the first suggestion DOM element
      }
      else {
        // show the first suggestion DOM element
        // and render the data
      }
    });

    你可以在这里看到可演示的示例工程

    以上的代码片段虽小但做到很多事:它适当的使用关注分离(separation of concerns)原则的实现了对多个事件流的管理,甚至做到了响应数据的缓存。这种函数式的风格使得代码看起来更像是声明式编程而非命令式编程:我们并不是在给一组指令去执行,只是定义了事件流之间关系来告诉它这是什么。例如,我们用Rx来告诉计算机suggestion1Stream'close 1'事件结合从最新的响应数据中拿到的一个用户数据的数据流,除此之外,当刷新事件发生时和程序启动时,它就是null

    留意一下代码中并未出现例如ifforwhile等流程控制语句,或者像JavaScript那样典型的基于回调(callback-based)的流程控制。如果可以的话(稍候会给你留一些实现细节来作为练习),你甚至可以在subscribe()上使用 filter()函数来摆脱ifelse。在Rx里,我们有例如: mapfilterscanmergecombineLateststartWith等数据流的函数,还有很多函数可以用来控制事件驱动编程(event-driven program)的流程。这些函数的集合可以让你使用更少的代码实现更强大的功能。

    接下来

    如果你认为Rx将会成为你首选的响应式编程库,接下来就需要花一些时间来熟悉一大批的函数用来变形、联合和创建被观察者。如果你想在事件流的图表当中熟悉这些函数,那就来看一下这个:RxJava's very useful documentation with marble diagrams。请记住,无论何时你遇到问题,可以画一下这些图,思考一下,看一看这一大串函数,然后继续思考。以我个人经验,这样效果很有效。

    一旦你开始使用了Rx编程,请记住,理解Cold vs Hot Observables的概念是非常必要的,如果你忽视了这一点,它就会反弹回来并残忍的反咬你一口。我这里已经警告你了,学习函数式编程可以提高你的技能,熟悉一些常见问题,例如Rx会带来的副作用

    但是响应式编程库并不仅仅是Rx,还有相对容易理解的,没有Rx那些怪癖的Bacon.jsElm Language则以它自己的方式支持响应式编程:它是一门会编译成Javascript + HTML + CSS的响应式编程语言,并有一个time travelling debugger功能,很棒吧。

    而Rx对于像前端和App这样需要处理大量的编程效果是非常棒的。但是它不只是可以用在客户端,还可以用在后端或者接近数据库的地方。事实上,RxJava就是Netflix服务端API用来处理并行的组件。Rx并不是局限于某种应用程序或者编程语言的框架,它真的是你编写任何事件驱动程序,可以遵循的一个非常棒的编程范式。

    如果这篇教程对你有帮助, 那么就请来转发一下吧(tweet it forward).

    原文链接


    展开全文
  • 响应式编程就是利用异步数据流进行编程,本质上就是观察者(Observer)模式的一种表现形式。我们首先讨论实现异步操作的几种常见方式,然后引出响应式编程的主流实现技术。 1. 实现异步的常见方式 在Java中,为了...

    响应式编程就是利用异步数据流进行编程,本质上就是观察者(Observer)模式的一种表现形式。我们首先讨论实现异步操作的几种常见方式,然后引出响应式编程的主流实现技术。

    1. 实现异步的常见方式

    在Java中,为了实现异步非阻塞,一般会采用回调(Callback)和Future这两种机制,但这两种机制都存在一定局限性。

    (1)回调

    回调的含义如下图所示,即类A的methodA()方法调用类B的methodB()方法,然后类B的methodB ()方法执行完毕后再主动调用类A的callback()方法。回调体现的是一种双向的调用方式。

    可以看到回调在任务执行过程中不会造成任何的阻塞,任务结果一旦就绪,回调就会被执行。但是我们也应该看到在使用回调机制时,代码会从一个类中的某个方法跳到另一个类中的某个方法,从而造成流程的不连续性。对于单层的异步执行而言,回调很容易使用。但是对于嵌套的多层异步组合而言就显得非常笨拙。所以回调很难大规模的组合起来使用,因为很快就会导致代码难以理解和维护,即形成所谓的“回调地狱(Callback Hell)”问题。

    (2)Future

    可以把Future模式简单理解为这样一种场景:我们有一个希望处理的任务,然后把这个任务提交到Future,Future就会在一定时间内完成这个任务,而在这段时间内我们可以去做其它事情。作为Future模式的实现,Java中的Future接口只包含如下5个方法。

    public interface Future<V> {

        boolean cancel(boolean mayInterruptIfRunning);

        boolean isCancelled();

        boolean isDone();

        V get() throws InterruptedException, ExecutionException;

        V get(long timeout, TimeUnit unit)?

        throws InterruptedException, ExecutionException, TimeoutException;

    }

     

    Future接口中的cancel()方法用于取消任务的执行;isCancelled()方法用于判断任务是否已经取消;两个get()方法会等待任务执行结束并获取结果,区别在于是否可以设置超时时间;最后isDone()方法判断任务是否已经完成。

    Future虽然可以实现获取异步执行结果的需求,但是它没有提供通知机制,我们无法得知Future什么时候完成。为了获取结果,我们要么使用阻塞的两种get()方法等待Future结果的返回,这时相当于执行同步操作;要么使用isDone()方法轮询地判断Future是否完成,这样会耗费CPU资源。所以,Future适合单层的简单调用,对于嵌套的异步调用而言同样非常笨重,不适合复杂的服务链路构建。

    鉴于Future机制存在的缺陷,Java 8中引入了CompletableFuture机制。CompletableFuture一定程度上弥补了普通Future的缺点。在异步任务完成后,我们使用任务结果时则不需要等待。可以直接通过thenAccept()、thenApply()、thenCompose()等方法将前面异步处理的结果交给另外一个异步事件处理线程来处理。

    CompletableFuture提供了非常强大的Future扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture所提供的各种方法。

    对于日常的开发工作而言,大多数时候我们是在处理简单的任务,这个时候使用CompletableFuture确实可以满足需求。但是,当系统越来越复杂,或者我们需要处理的本身就是一个非常复杂的任务时, CompletableFuture对于多个处理过程的组合仍然不够便捷。使用CompletableFuture编排多个Future是可行的,但并不容易。我们会担心写出来的代码是否真的没有问题,而随着时间的推移,这些代码会变得越来越复杂和难以维护。为此,我们需要引入响应式编程的相关技术和框架,这些技术和框架能够支持在未来更轻松地维护异步处理代码。

    2. 响应式编程的主流实现技术

    目前,响应式编程的主流实现技术包括RxJava、Akka Streams、Vert.x和Project Reactor等。

    (1)RxJava

    Reactive Extensions(Rx)是一个类库,它集成了异步、基于可观察(Observable)序列的事件驱动编程,最早应用于微软的.NET平台。而RxJava是Reactive Extensions的Java实现,用于通过使用Observable/Flowable序列来构建异步和基于事件的程序库,目前有1.x版本和2.x版本两套实现。

    RxJava 1.x版本诞生于响应式流规范之前,虽然可以和响应式流的接口进行转换,但是由于底层实现的原因,使用起来并不是很直观。RxJava 2在设计和实现时考虑到了与现有规范的整合,按照响应式流规范对接口进行了重写,并把1.x版本中的背压功能单独分离出来。但为了保持与RxJava 1.x版本的兼容性,RxJava 2在很多地方的使用也并不直观。关于RxJava的更多内容可参考官网(http://reactivex.io/)。

    (2)Akka Streams

    Akka运行在JVM上,是构建高并发、分布式和高弹性的消息驱动应用程序的一个工具套件。Actor是Akka中最核心的概念,它是一个封装了状态和行为的对象,Actor之间可以通过交换消息的方式进行通信。通过Actor能够简化锁及线程管理,可以非常容易地开发出正确的并发程序和并行系统。

    Akka也是响应式流规范的初始成员,而Akka Streams是以Akka为基础的响应式流的实现,在Akka现有的角色模型之上提供了一种更高层级的抽象,支持背压等响应式机制。

    (3)Vert.x

    Vert.x是Eclipse基金会下的一个开源的Java工具,是一个异步网络应用开发框架,用来构建高并发、异步、可伸缩、多语言支持的Web应用程序。Vert.x就是为了构建响应式系统而设计,基于事件驱动架构,Vert.x实现了非阻塞的任务处理机制。

    Vert.x中包含Vert.x Reactive Streams工具库,该工具库提供了Vert.x上响应式流规范的实现。我们可以通过Vert.x提供的可读流和可写流处理响应式流规范中的发布者和订阅者。

    (4)Project Reactor

    Spring 5中引入了响应式编程机制,而Spring 5中默认集成了Project Reactor作为该机制的实现框架。Reactor诞生较晚,可以认为是第二代响应式开发框架。所以它是一款完全基于响应式流规范设计和实现的工具库,没有RxJava那样的历史包袱,在使用上更加的直观易懂。但从设计理念和API的表现形式上,Reactor与RxJava比较类似,可以说Reactor基于响应式流规范,但在API方面又尽可能向RxJava靠拢。

    Flux和Mono是Reactor中的两个核心组件,Flux代表包含0到n个元素的异步序列,而Mono则表示包含0个或1个元素的异步序列。

     

    如果对文章感兴趣,可以关注我的微信公众号:程序员向架构师转型。

    我出版了《系统架构设计:程序员向架构师转型之路》、《向技术管理者转型:软件开发人员跨越行业、技术、管理的转型思维与实践》、《微服务设计原理与架构》、《微服务架构实战》等书籍,并翻译有《深入RabbitMQ》和《Spring5响应式编程实战》,欢迎交流。

    展开全文
  • 使用ReactiveCocoa实现iOS平台响应式编程 ReactiveCocoa和响应式编程 在说ReactiveCocoa之前,先要介绍一下FRP(Functional Reactive Programming,响应式编程),在维基百科中有这样一个例子介绍: 在命令式...

    使用ReactiveCocoa实现iOS平台响应式编程

    ReactiveCocoa和响应式编程

    在说ReactiveCocoa之前,先要介绍一下FRP(Functional Reactive Programming,响应式编程),在维基百科中有这样一个例子介绍:

    在命令式编程环境中,a = b + c 表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。

    Excel就是响应式编程的一个例子。单元格可以包含字面值或类似”=B1+C1″的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。

    而ReactiveCocoa简称RAC,就是基于响应式编程思想的Objective-C实践,它是Github的一个开源项目,你可以在这里找到它。

    关于FRP和ReactiveCocoa可以去看leezhong的这篇blog,图文并茂,讲的很好。

    ReactiveCocoa框架概览

    先来看一下leezhong再博文中提到的比喻,让你对有个ReactiveCocoa很好的理解:

    可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。可以在水龙头上加一个过滤嘴(filter),不符合的不让通过,也可以加一个改动装置,把球改变成符合自己的需求(map)。也可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),这样只要其中的一个水龙头有玻璃球出来,这个新合并的水龙头就会得到这个球。

    下面我来逐一介绍ReactiveCocoa框架的每个组件

    Streams

    Streams 表现为RACStream类,可以看做是水管里面流动的一系列玻璃球,它们有顺序的依次通过,在第一个玻璃球没有到达之前,你没法获得第二个玻璃球。
    RACStream描述的就是这种线性流动玻璃球的形态,比较抽象,它本身的使用意义并不很大,一般会以signals或者sequences等这些更高层次的表现形态代替。

    Signals

    Signals 表现为RACSignal类,就是前面提到水龙头,ReactiveCocoa的核心概念就是Signal,它一般表示未来要到达的值,想象玻璃球一个个从水龙头里出来,只有了接收方(subscriber)才能获取到这些玻璃球(value)。

    Signal会发送下面三种事件给它的接受方(subscriber),想象成水龙头有个指示灯来汇报它的工作状态,接受方通过-subscribeNext:error:completed:对不同事件作出相应反应

    • next 从水龙头里流出的新玻璃球(value)
    • error 获取新的玻璃球发生了错误,一般要发送一个NSError对象,表明哪里错了
    • completed 全部玻璃球已经顺利抵达,没有更多的玻璃球加入了

    一个生命周期的Signal可以发送任意多个“next”事件,和一个“error”或者“completed”事件(当然“error”和“completed”只可能出现一种)

    Subjects

    subjects 表现为RACSubject类,可以认为是“可变的(mutable)”信号/自定义信号,它是嫁接非RAC代码到Signals世界的桥梁,很有用。嗯。。。 这样讲还是很抽象,举个例子吧:

    1
    2
    3
    RACSubject *letters = [RACSubject subject];
    RACSignal *signal = [letters sendNext:@"a"];
     

    可以看到@"a"只是一个NSString对象,要想在水管里顺利流动,就要借RACSubject的力。

    Commands

    command 表现为RACCommand类,偷个懒直接举个例子吧,比如一个简单的注册界面:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        RACSignal *formValid=[RACSignal
            combineLatest:@[
                self.userNameField.rac_textSignal,
                self.emailField.rac_textSignal,
            ]
            reduce:^(NSString *userName,NSString *email){
                return@(userName.length&gt;0
                        &amp;&amp;email.length&gt;0);
            }];
     
      RACCommand *createAccountCommand=[RACCommandcommandWithCanExecuteSignal:formValid];
      RACSignal *networkResults=[[[createAccountCommand
          addSignalBlock:^RACSignal *(idvalue){
              //... 网络交互代码
          }]
          switchToLatest]
          deliverOn:[RACSchedulermainThreadScheduler]];
     
      // 绑定创建按钮的 UI state 和点击事件
        [[self.createButtonrac_signalForControlEvents:UIControlEventTouchUpInside]executeCommand:createAccountCommand];
     

    Sequences

    sequence 表现为RACSequence类,可以简单看做是RAC世界的NSArray,RAC增加了-rac_sequence方法,可以使诸如NSArray这些集合类(collection classes)直接转换为RACSequence来使用。

    Schedulers

    scheduler 表现为RACScheduler类,类似于GCD,but schedulers support cancellationbut schedulers support cancellation, and always execute serially.

    ReactiveCocoa的简单使用

    实践出真知,下面就举一些简单的例子,一起看看RAC的使用

    Subscription

    接收 -subscribeNext: -subscribeError: -subscribeCompleted:


    1
    2
    3
    4
    5
    6
    7
    RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
     
    // 依次输出 A B C D…
    [letters subscribeNext:^(NSString *x) {
        NSLog(@"%@", x);
    }];
     

    Injecting effects

    注入效果 -doNext: -doError: -doCompleted:,看下面注释应该就明白了:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    __blockunsignedsubscriptions=0;
     
    RACSignal *loggingSignal=[RACSignalcreateSignal:^RACDisposable *(id&lt;RACSubscriber&gt;subscriber){
        subscriptions++;
        [subscribersendCompleted];
        returnnil;
    }];
     
    // 不会输出任何东西
    loggingSignal=[loggingSignaldoCompleted:^{
        NSLog(@"about to complete subscription %u",subscriptions);
    }];
     
    // 输出:
    // about to complete subscription 1
    // subscription 1
    [loggingSignalsubscribeCompleted:^{
        NSLog(@"subscription %u",subscriptions);
    }];
     

    Mapping

    -map: 映射,可以看做对玻璃球的变换、重新组装


    1
    2
    3
    4
    5
    6
    7
    RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
     
    // Contains: AA BB CC DD EE FF GG HH II
    RACSequence *mapped = [letters map:^(NSString *value) {
        return [value stringByAppendingString:value];
    }];
     

    Filtering

    -filter: 过滤,不符合要求的玻璃球不允许通过


    1
    2
    3
    4
    5
    6
    7
    RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;
     
    // Contains: 2 4 6 8
    RACSequence *filtered=[numbersfilter:^BOOL(NSString *value){
        return(value.intValue%2)==0;
    }];
     

    Concatenating

    -concat: 把一个水管拼接到另一个水管之后


    1
    2
    3
    4
    5
    6
    RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
     
    // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
    RACSequence *concatenated = [letters concat:numbers];
     

    Flattening

    -flatten:

    Sequences are concatenated


    1
    2
    3
    4
    5
    6
    7
    RACSequence *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *sequenceOfSequences=@[letters,numbers].rac_sequence;
     
    // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
    RACSequence *flattened=[sequenceOfSequencesflatten];
     

    Signals are merged (merge可以理解成把几个水管的龙头合并成一个,哪个水管中的玻璃球哪个先到先吐哪个玻璃球)


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id&lt;RACSubscriber&gt; subscriber) {
        [subscriber sendNext:letters];
        [subscriber sendNext:numbers];
        [subscriber sendCompleted];
        return nil;
    }];
     
    RACSignal *flattened = [signalOfSignals flatten];
     
    // Outputs: A 1 B C 2
    [flattened subscribeNext:^(NSString *x) {
        NSLog(@"%@", x);
    }];
     
    [letters sendNext:@"A"];
    [numbers sendNext:@"1"];
    [letters sendNext:@"B"];
    [letters sendNext:@"C"];
    [numbers sendNext:@"2"];
     

    Mapping and flattening

    -flattenMap: 先 map 再 flatten


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;
     
    // Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
    RACSequence *extended=[numbersflattenMap:^(NSString *num){
        return@[num,num].rac_sequence;
    }];
     
    // Contains: 1_ 3_ 5_ 7_ 9_
    RACSequence *edited=[numbersflattenMap:^(NSString *num){
        if(num.intValue%2==0){
            return[RACSequenceempty];
        }else{
            NSString *newNum=[numstringByAppendingString:@"_"];
            return[RACSequencereturn:newNum];
        }
    }];
     
     
     
     
    RACSignal *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence.signal;
     
    [[letters
        flattenMap:^(NSString *letter){
            return[databasesaveEntriesForLetter:letter];
        }]
        subscribeCompleted:^{
            NSLog(@"All database entries saved successfully.");
        }];
     

    Sequencing

    -then:


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
     
    // 新水龙头只包含: 1 2 3 4 5 6 7 8 9
    //
    // 但当有接收时,仍会执行旧水龙头doNext的内容,所以也会输出 A B C D E F G H I
    RACSignal *sequenced = [[letters
        doNext:^(NSString *letter) {
            NSLog(@"%@", letter);
        }]
        then:^{
            return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
        }];
     

    Merging

    +merge: 前面在flatten中提到的水龙头的合并


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    RACSubject *letters=[RACSubjectsubject];
    RACSubject *numbers=[RACSubjectsubject];
    RACSignal *merged=[RACSignalmerge:@[letters,numbers]];
     
    // Outputs: A 1 B C 2
    [mergedsubscribeNext:^(NSString *x){
        NSLog(@"%@",x);
    }];
     
    [letterssendNext:@"A"];
    [numberssendNext:@"1"];
    [letterssendNext:@"B"];
    [letterssendNext:@"C"];
    [numberssendNext:@"2"];
     

    Combining latest values

    +combineLatest: 任何时刻取每个水龙头吐出的最新的那个玻璃球


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSignal *combined = [RACSignal
        combineLatest:@[ letters, numbers ]
        reduce:^(NSString *letter, NSString *number) {
            return [letter stringByAppendingString:number];
        }];
     
    // Outputs: B1 B2 C2 C3
    [combined subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
     
    [letters sendNext:@"A"];
    [letters sendNext:@"B"];
    [numbers sendNext:@"1"];
    [numbers sendNext:@"2"];
    [letters sendNext:@"C"];
    [numbers sendNext:@"3"];
     

    Switching

    -switchToLatest: 取指定的那个水龙头的吐出的最新玻璃球


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    RACSubject *letters=[RACSubjectsubject];
    RACSubject *numbers=[RACSubjectsubject];
    RACSubject *signalOfSignals=[RACSubjectsubject];
     
    RACSignal *switched=[signalOfSignalsswitchToLatest];
     
    // Outputs: A B 1 D
    [switchedsubscribeNext:^(NSString *x){
        NSLog(@"%@",x);
    }];
     
    [signalOfSignalssendNext:letters];
    [letterssendNext:@"A"];
    [letterssendNext:@"B"];
     
    [signalOfSignalssendNext:numbers];
    [letterssendNext:@"C"];
    [numberssendNext:@"1"];
     
    [signalOfSignalssendNext:letters];
    [numberssendNext:@"2"];
    [letterssendNext:@"D"];
     

    常用宏

    RAC 可以看作某个属性的值与一些信号的联动


    1
    2
    3
    4
    RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal]reduce:^id(NSString *userName, NSString *password) {
        return @(userName.length &gt;= 6 &amp;&amp; password.length &gt;= 6);
    }];
     

    RACObserve 监听属性的改变,使用block的KVO


    1
    2
    3
    4
    [RACObserve(self.textField,text)subscribeNext:^(NSString *newName){
        NSLog(@"%@",newName);
    }];
     

    UI Event

    RAC为系统UI提供了很多category,非常棒,比如UITextView、UITextField文本框的改动rac_textSignal,UIButton的的按下rac_command等等。

    最后

    有了RAC,可以不用去操心值什么时候到达什么时候改变,只需要简单的进行数据来了之后的步骤就可以了。

    说了这么多,在回过头去看leezhong的比喻该文最后总结的关系图,再好好梳理一下吧。我也是初学者,诚惶诚恐的呈上这篇博文,欢迎讨论,如有不正之处欢迎批评指正。

    参考

    https://github.com/ReactiveCocoa/ReactiveCocoa

    https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/FrameworkOverview.md

    https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md

    http://vimeo.com/65637501
    http://iiiyu.com/2013/09/11/learning-ios-notes-twenty-eight/
    http://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.htmlhttp://nshipster.com/reactivecocoa/

    展开全文
  • Vue响应式编程

    千次阅读 2019-09-11 18:49:18
    ,等到后面想用的时候,直接修改即可,反正它一直都是响应式的。 但往未来预先定义一步,并不是那么冗余的,这牵涉到预测未来了。 第二种:用 Object.assign() 创建新的对象以覆盖原来的对象。 vm . ...

    Time: 20190911

    到本篇文章为止,我们在前面的文章中学习了如何将JS中的数据传到HTML模板中,最终转化为DOM在浏览器显示。

    Vue在这个过程中扮演了怎样的角色呢?

    Vue会时刻监视着data对象的变化。下面看一个例子:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> 生产环境 -->
    </head>
    <body>
        <div id="app">
            <p>自从打开网页,已经过去{{ seconds }}秒。</p>
        </div>
    
        <script>
            var app = new Vue(
                {
                    el: '#app',
                    data: {
                        seconds: 0
                    },
    
                    created() {
                        // 应用启动时执行
                        console.log("应用启动...")
                        setInterval(() => {
                            this.seconds++
                        }, 1000)
                    }
                }
            )
        </script>
    </body>
    </html>
    

    执行效果

    就是个网页计数器,每一秒数字变化一次。

    这里用到了created生命周期钩子函数,这也是Vue框架的核心内容,会在后面继续展开。this.seconds会指向data对象中的变量,改变这个变量,相应的模板也会变化。

    非常简洁的语法。

    到目前为止,写Vue的代码都给人一种非常灵活的感觉。

    上面这个例子展示的是Vue的响应式能力,用的是数值,也可以对v-bind的属性进行操作:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> 生产环境 -->
    </head>
    <body>
        <div id="app">
            <p>自从打开网页,已经过去{{ seconds }}秒。</p>
            <button :disabled="buttonDisabled">测试按钮</button>
        </div>
    
        <script>
            var app = new Vue(
                {
                    el: '#app',
                    data: {
                        seconds: 0,
                        buttonDisabled: true
                    },
    
                    created() {
                        // 应用启动时执行
                        console.log("应用启动...")
                        setInterval(() => {
                            this.seconds++,
                            this.buttonDisabled = !this.buttonDisabled
                        }, 1000)
                    }
                }
            )
        </script>
    </body>
    </html>
    

    效果如下:

    在这里插入图片描述

    在这里插入图片描述
    按钮在可用和不可用之间一秒一次切换。

    到这里为止,应该可以感受到响应式的能力。响应式的实现原理是怎样的呢?后续会发一篇深入理解响应式实现原理的文章,先留空。

    响应式的限制

    为对象添加新的属性,并不是响应式的

    const vm = new Vue({
    	data: {
    		formData: {
    			username: 'xxx'
    		}
    	}
    })
    vm.formData.name = 'new user'
    

    原先定义的username是响应式的,即该属性的变化会触发模板更新。

    新定义的不是响应式的。

    有什么解决办法呢?主要有三种:

    • 第一种:预先为name留个位置,就像我上面为要写的文章留个空一样,设置为undefined,等到后面想用的时候,直接修改即可,反正它一直都是响应式的。

    但往未来预先定义一步,并不是那么冗余的,这牵涉到预测未来了。

    • 第二种:用Object.assign()创建新的对象以覆盖原来的对象。
    vm.formData = Object.assign({}, vm.formData, {name: 'new user'})
    
    • 第三种:Vue.set()方法,可以将属性值设置为响应式的
    Vue.set(vm.formData, 'name', 'new user')
    

    关于设置数组元素

    直接按照下标索引修改是不可行的(??)。

    const vm = new Vue({
    	data: {
    		dogs: ['Rex', 'Rover', 'Alan'
    	}
    })
    
    vm.dogs[2] = 'Bob' // 错误
    

    改进的方式有两种:

    • .splice()函数, vm.dogs.splice(2,1, 'Bob')
    • Vue.set()函数,Vue.set(vm.dogs, 2, 'Bob')

    两种方法效果相同。

    设置数组长度

    也是用.splice()函数,不过这个方法只能用于缩短长度,不能扩展长度。

    END.

    展开全文
  • 同时也可以参考我前面的两篇翻译《响应式编程框架ReactiveCocoa学习——基本操作符》《响应式编程框架ReactiveCocoa介绍与入门》。其中ReactiveCocoa的Github官方地址为 https://github.com/ReactiveCocoa/React
  • ReactiveCocoa是Github团队开发的第三方函数式响应式编程框架,在目前市面上的很多iOS App都大量使用了这个框架。以下我简称这个框架为RAC.我下面会通过几篇博客来和大家一起学习这个强大的框架。该博客的案例代码...
  • 响应式编程,现在被经常提起,同时越来越多的出现在我们的代码构建中。同时现在有很多主流的响应式框架,如RX等,如果不能够理解响应式编程的话,对此类框架的使用总是有一些迷惑。那么,到底什么是响应式编程? ...
  • 我在上一篇博客中《响应式编程框架ReactiveCocoa介绍与入门》简单介绍了ReactiveCocoa的介绍和简单使用,主要是翻译了官方文档中的README部分,其实个人认为技术最好的学习方式就是去看官方文档。今天我仍旧来翻译...
  • springboot2 webflux 响应式编程学习路径

    千次阅读 2018-05-05 14:41:19
    springboot2 已经发布,其中最亮眼的非webflux响应式编程莫属了!响应式的weblfux可以支持高吞吐量,意味着使用相同的资源可以处理更加多的请求,毫无疑问将会成为未来技术的趋势,是必学的技术!很多人都看过相关的...
  • 那些年我们错过的响应式编程

    千次阅读 2015-12-21 09:32:59
    我在不断的构建(building)中把响应式编程了解的透彻了一些,最后以这种艰难的方式学完了响应式编程。在我工作公司的一个真实项目中我会用到它,当我遇到问题时,还可以得到同事的支持。 学习过程中最难的部分是 如何...
  • 为什么响应式编程并非一时之势?

    千次阅读 2016-06-08 11:33:17
    本文作者为 David Buschman,文章从程序架构与系统的发展历程出发,逐步论证了为什么响应式编程并非一时之势,而是能带来更快处理速度,更高硬件利用率的未来选择。文章系国内 ITOM 管理平台 OneAPM 编译呈现。
  • FRP 是一种陈述的 GUI 程序设计方法,这和传统的编程方法有很大不同。传统方法关注‘what’和‘how’,而一个陈述的语言让你可以只说明显示什么内容,而不用去编程告诉计算机具体怎么做。 当前多数 GUI 编程...
  • 最近给小伙伴分享了Rxjava的源码解读,并录制成视频,也是为了配合自己的未来出版的书,也是对书的内容的补充,将未能写进去的内容通过视频来...02 Java9中的响应式编程:www.bilibili.com/video/av345… 03 Rxjava...
  • 接上一篇: ...响应式编程是一种编程模型,本节将介绍这种编程模型的具体实现工具 Project Reactor框架。 Reactor框架也是 Spring5中实现响应式编程采用的默认框架。 Project Reactor: https://pro...
  • Reactive(2) 响应式流与制奶厂业务

    千次阅读 2019-09-23 17:26:39
    目录 再谈响应式 为什么Web后端开发的,对 Reactive 没有感觉 Java 9 支持的 Reactive Stream 范例 小结 扩展阅读 再谈响应式 ...在前一篇文章从Reactive编程到“好莱坞”中,...响应式编程强调的是异步化、面...
  • 理解反应式编程 你曾有过订阅报纸或者杂志的经历吗?互联网的确从传统的出版发行商那儿分得了一杯羹,但是过去订阅报纸真的是我们了解时事的最佳方式。那时,我们每天早上都会收到一份新鲜出炉的报纸,并在...
  • HTML5被称为是web开发的未来可不是空穴来风。...如今已经是响应式网站设计的时代,用户不再仅通过宽大的电脑屏幕来访问网站了。框架能够大幅简化设计者和开发者的工作。笔者搜集了10个广受好评的HTML5响应
  • 当前响应式编程的实现有什么问题? 关于React式编程、期货和承诺的大惊小怪已经困扰了我很长时间。 无数文章和博客文章宣传使用 promise 作为异步回调地狱的治疗方法,但这些框架生成的代码在我看来并不干净或易于...
  • 微服务不是一个新事物,1970年就出现了,如今右变得流行。因为它可以快速构建或修改出有价值的产品...(例如bootstrap根据屏幕的大小改变布局)响应式编程-数据驱动响应式系统-异步消息 让我们看看RxJava(Reactive...
  • 函数式编程的优点

    千次阅读 2020-06-11 04:18:24
    在本系列的第一部分中 ,我讨论了函数式编程的核心概念,并提供了一些示例说明它们如何发挥作用。 函数编程的核心概念列表(再次从第一部分开始)如下: 使用功能作为其他功能的输入和输出, 高阶函数 使用map ...
  • 西方著名宗教史家米尔恰·伊利亚德说,如果今天我们不生活在未来,那么未来,我们会生活在...今天我们从推送的历史开始说起,介绍一下目前常见的推送实现方式,并结合反应的例子来继续实战反应编程模型的应...
  • 1. 前言响应式编程已经在 Java 编程领域出现很长一段时间了。具有高性能,事件驱动,充分利用计算资源,更加优雅的异步编程体验,同时它也提供了背压机制...
  • 杨绛先生说:大部分人的问题是,做得不多而想得太多。 今天要讲的函数式编程可能和Spring Boot本身的关系不太大,但是它...Spring 5中的引入了对响应式编程的支持——WebFlux,它基于Reactor类库的基础实现,之前的...
  • #iOS 上的函数式响应式编程 大多数 iOS 应用程序都有多层状态。 在这些层之间手动传达状态更改容易出错,并且往往会产生难以维护的代码。 Functional Reactive Programming 提供了一个工具集来声明不同对象之间的...
  • 总第264篇2018年 第56篇前言EasyReact 是一款基于响应式编程范式的客户端开发框架,开发者可以使用此框架轻松地解决客户端的异步问题。目前 EasyReact...
  • 响应式Web设计实践》学习笔记

    千次阅读 2017-01-02 10:39:52
    1.4 成为响应式的 Ethan Marcotte利用三种已有工具:媒介查询(media queries)、流动布局(fluid grids)和自适应图片(scalable images)创建了一个在不同分辨率屏幕下都能漂亮地显示的站点。 设备无关:所有的...
  • Atitit 提升开发效率 声明式编程范式 目录 1. 声明式编程体系树 1 1.1. 声明式(对比:指令式,,主要包括 函数式,逻辑式编程) 2 1.2. 声明式编程:表达与运行分离 3 1.3. 不仅表达当下,还表达未来 3 1.4...
  • EasyReact 是一款基于响应式编程范式的客户端开发框架,开发者可以使用此框架轻松地解决客户端的异步问题。 目前 EasyReact 已在美团和大众点评客户端的部分业务中实践,并且持续迭代了一年多的时间。近日,我们...
  • RSocket一种新的响应式应用新协议

    千次阅读 2019-03-07 00:45:33
    反应式编程(响应式reactive)是 Java 中高效应用的下一个前沿。但有两个主要障碍 - 数据访问和网络 。RSocket旨在解决后一个问题,而R2DBC旨在解决前者问题。 HTTP vs RSocket HTTP的一个重要问题是,它强迫...
  • 现实生活中的例子反应式编程

    千次阅读 2017-03-14 15:18:39
    编程教育被动做法 - 一个相当困难的事情,而且缺乏教材只会加剧这一进程。大多数现有的培训手册不提供深入的审查,并讨论如何设计项目作为一个整体的架构。 这种材料的目的是帮助初学者开始思考真正的“反应”。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 32,192
精华内容 12,876
关键字:

响应式编程是未来