Slug max frp rxswift

RxSwift 函数响应式编程

异步操作的代码为什么会成为程序员的梦魇呢?函数响应式编程 (Functional Reactive Programming) 旨在简化异步操作,让您可以像操作变量一样来操作闭包。RxSwift 是一个全新的第三方库,让您的事件驱动 (event-driven) 应用更容易进行管理,增强代码的可读性,从而减少错误的发生,让您不再为此而烦恼。Max Alexander 为大家展示了这个库的基本操作,并讲述了如何使用函数响应式编程来达成我们的目的等内容。


概述 (0:00)

这几年有很多人在讨论着关于 Rx 的相关概念。Rx 通过 Observable<Element> 接口来表达计算型泛型抽象 (generic abstraction of computation) 的概念,而 RxSwift 是 Rx 的 Swift 版本。无疑,这个内容非常庞大,所以我打算用一种稍微简单点的介绍方式来讲解这个框架。

如果有人写出了下面这么一段代码,相信肯定会有很多人感到非常不爽:

Alamofire.request(.POST, "login", parameters: ["username": "max", "password": "insanity"])
  .responseJSON(completionHandler: { (firedResponse) -> Void in
    Alamofire.request(.GET, "myUserInfo" + firedResponse.result.value)
      .responseJSON(completionHandler: { myUserInfoResponse in
        Alamofire.request(.GET, "friendList" + myUserInfoResponse.result.value)
          .responseJSON(completionHandler: { friendListResponse in
            Alamofire.request(.GET, "blockedUsers" + friendListResponse.result.value)
              .responseJSON(completionHandler: {

              })
            })
          })
    Alamofire.request(.GET, "myUserAcccount" + firedResponse.result.value)
      .responseJSON(completionHandler: {
      })
    })

这段代码有什么问题呢?这些都是 Alamofire 网络请求方法。如果大家没用过 Alamofire 的话,可以把它看作是 AFNetworking 框架在执行 HTTP 请求操作。在这段代码中你可以看到各种各样的嵌套闭包代码,每多一层嵌套,代码缩进就会向右移动一层,你知道这段代码是有问题的,因为你无法直观地解释这段代码做了什么。在这段代码中涉及到了不少网络服务,而网络很有可能会访问失败。我们需要为此添加错误处理,然而我们却无法知道该在何处处理这些所有的错误异常。Rx 就应运而生,它可以帮助我们解决这个问题。

回到基础 (2:26)

在处理不同事件的时候,无论如何你都会持有一个包含这些事件的集合。举个例子,我们有这样一个整数数组 [1, 2, 3, 4, 5, 6],如果你喜欢你也可以称之为列表。当我需要执行某些操作的时候,最符合 Swift 风格的操作就是使用 filter 方法了。

Receive news and updates from Realm straight to your inbox

[1, 2, 3, 4, 5, 6].filter{ $0 % 2 == 0 }

那么如果我想要给这个数组中的所有元素都乘以 5 然后生成一个新数组呢?

[1, 2, 3, 4, 5, 6].map{ $0 * 5 }

那么执行加法操作呢?

[1, 2, 3, 4, 5, 6].reduce(0, +)

这些操作非常简便。我们没有使用任何 for 循环方法,我们也不会持有并保持那些临时的中间数。这看起来就非常像 Scala 或者 Haskel 的操作方式。然而,一个正常的应用程序很难只使用数组就可以完成的了。大家都希望使用网络、下载图片、网上聊天、添加好友等等。你需要大量使用 IO 操作。IO 意味着你需要让内存与用户交互动作、用户设备、相机、硬盘等等诸如此类的东西进行数据交换。这些操作都是异步的,随时都可能会发生失败,并且会发生很多奇怪的问题。

Rx 权利法案 (4:09)

我提出了一个关于 Rx 的权利法案,它规定:

我们的开发者拥有像管理迭代集合 (iterable collections) 一样管理异步事件的权利。

观察者 (4:25)

在 Rx 的世界里,让我们用观察者 (Observables) 的概念来代替数组。观察者是一个类型安全的事件对象,可以长期对不同种类的数据值进行写入和读出。RxSwift 目前处于 Beta 3 的版本,安装非常简单。你所需要做的就是导入 RxSwift 即可。

pod 'RxSwift', '~> 2.0.0-beta.3'
import RxSwift

创建观察者也很容易。最简单的创建方式就是使用 just,这是一个 RxSwift 内建的函数。你可以将你所想要的变量放到其中,它就会返回一个包含相同类型的观察者变量。

just(1)  //Observable<Int>

那么如果我们想要从数组中一个接一个的推出元素并执行相关操作呢?

[1,2,3,4,5,6].toObservable()  //Observable<Int>

这会返回一个 Observable<Int> 对象。

如果你在使用类似于上传数据到 S3 或者向本地数据库保存数据之类的 API,你可以这样写:

create { (observer: AnyObserver<AuthResponse>) -> Disposable in

  return AnonymousDisposable {

  }
}

当你调用 create 的时候会返回一个闭包。这个闭包会给予一个观察者 参数,这意味着有某个东西正在对其进行观察。目前你可以忽略 AnonymousDisposable 这个东西。在下面两行代码中你将会看到你在何处将 API 代码转换为了一个好用的观察者对象。

下面这段代码和 Alamofire 的使用方式相同:

create { (observer: AnyObserver<AuthResponse>) -> Disposable in

  let request = MyAPI.get(url, ( (result, error) -> {
    if let err = error {
      observer.onError(err);
    }
    else if let authResponse = result {
      observer.onNext(authResponse);
      observer.onComplete();
    }
  })
  return AnonymousDisposable {
    request.cancel()
  }
}

我可以进行日志记录操作,也可以执行一个 GET 请求,随后我可以得到一个带有结果和错误的回调函数。实际上我无法改变这个 API,因为这是由另一个客户端 SDK 所提供的,但是我可以将其转换为一个观察者对象。当存在错误的时候,我会调用 observer.onError()方法。这意味着只要监听了这个对象的代码都会接收到这个错误消息。当你得到可用的服务器回应时,调用 observable.onNext() 方法。接着,如果处理结束的话就调用 onComplete()。这个时候我们就到了 AnonymousDisposable 这里了。AnonymousDisposable 是当你想要中止请求的时候被调用的操作。比如说你离开了当前视图控制器或者应用已经不再需要调用这个请求的时候,就可以使用这个方法了。这对视频上传等大文件操作是非常有用的。当你结束所有操作的时候,request.cancel() 可以清除所有的资源。无论是操作完成还是发生错误,这个方法都会被调用。

监听观察者 (8:11)

现在我们知道如何创建观察者了,那么就来看看如何对其建立监听吧!我们以数组为例,因为我们可以在很多对象中调用一个名为 toObservable() 的扩展方法。随后,就可以编写监听函数了:

[1,2,3,4,5,6]
  .toObservable()
  .subscribeNext {
    print($0)
  }

这看起来跟枚举差不多。Subscribe 监听器事件基于失败请求、下一步事件以及 onCompleted 操作,给你提供了各种各样的信息。你可以有选择性的建立相应的监听:

[1,2,3,4,5,6]
  .toObservable()
  .subscribe(onNext: { (intValue) -> Void in
    // 推出一个整数数据
  }, onError: { (error) -> Void in
    // 发生错误!
  }, onCompleted: { () -> Void in
    // 没有更多的信号进行处理了
  }) { () -> Void in
    // 我们要清除这个监听器
  }

关联观察者 (9:14)

使用 Rx 的最好例子就是套接字 (socket) 服务了。假设我们有一个网络套接字服务,它用来监听股票行情,然后显示用户的当前账户余额 UI 界面。由于股票行情对应了不同的事件,根据这些事件来决定用户是否可以购买股票。如果账户余额过低的时候我们将禁止用户购买,当股票票价在用户的承担范围内的时候允许用户购买。

func rx_canBuy() -> Observable<Bool> {
  let stockPulse : [Observable<StockPulse>]
  let accountBalance : Observable<Double>

  return combineLatest(stockPulse, accountBalance,
    resultSelector: { (pulse, bal) -> Bool in
    return pulse.price < bal
  })
}

combineLatest 意味着当某个事件发生的时候,我们就将最近的两个事件之间建立关联。Redution 闭包是否会被回调取决于股票票价是否低于余额。如果被回调,这就意味着用户可以购买这只股票。这个操作允许你将两个观察者关联起来,然后列出一个逻辑决定某些操作是否可以进行。这会返回一个 Bool 类型的观察者。

rx_canBuy()
  .subscribeNext { (canBuy) -> Void in
    self.buyButton.enabled = canBuy
  }

使用 subscribe 方法来操作刚刚我创建的那个会返回 Bool 值的 rx_canBuy 方法。然后你就可以根据 canBuy 的返回值来决定 self.buyButton 的行为了。

让我们来举一个合并的例子。假设我有一个存有我所喜欢的股票的用户界面应用。我通过 Apple、Google 以及 Johnson 来监听股票票价。所有这些股票行情都有不同的结果。当股票行情发生变化的时候我需要立刻知道并更新我的用户界面。

let myFavoriteStocks : [Observable<StockPulse>]

myFavoriteStocks.merge()
  .subscribeNext { (stockPulse) -> Void in
    print("\(stockPulse.symbol)/
      updated to \(stockPulse.price)/")
  }

这些参数的类型都是相同的 Observable<StockPulse> 类型。我需要知道它们何时被触发,我需要做的就是持有一个观察者数组。里面存放了我需要进行监听的多个不同种类的股票行情,我可以在一个输出流中将它们合并然后进行监听。

与 Rx 观察者图表进行交互 (18:03)

我使用 Rx 使用了很长时间。很遗憾,我仍然忘记了许许多多的操作,并且需要非常频繁地回顾参考文档。这个网站 rxmarbles.com 将会为我们展示所有这些操作的理论部分。

轻松实现后台进程 (19:03)

借助 RxSwift 还有许多很赞的事情可以做。比如说你有一个视频上传操作,由于这个视频文件太大了,因此你需要在后台中进行。执行这个操作的最好办法就是使用 observeOn 进行。

let operationQueue = NSOperationQueue()
  operationQueue.maxConcurrentOperationCount = 3
  operationQueue.qualityOfService = NSQualityOfService.UserInitiated
  let backgroundWorkScheduler
    = OperationQueueScheduler(operationQueue: operationQueue)

videoUpload
  .observeOn(backgroundWorkScheduler)
  .map({ json in
    return json["videoUrl"].stringValue
  })
  .observeOn(MainScheduler.sharedInstance)
  .subscribeNext{ url
    self.urlLabel.text = url
  }

视频上传操作需要给我当前完成度百分比的信号信息,这样我才能知道这个操作是否完成。但是我并不打算在主线程中执行这个操作,因为我可以在后台执行。当视频上传结束之后,我会得到一个返回的 JSON 数据,它会告知我所上传的 URL 地址这样我就可以将其写入到 UI 标签当中了。因为我是在后台进行监听的,因此这个操作不会在主线程上进行。我需要通知 UI 进行更新,这样才能将信息传递给主线程。因此我们需要回去执行 observeOn(MainScheduler.SharedInstance) 方法,这个操作将会执行 UI 更新操作。遗憾的是,和 Android 上的 RxJava 框架不同,在 Swift 中如果你在后台进程中更新 UI 也是可以进行的(但不要这么做)。在 Android 中这么做会发生崩溃,而在 Swift 中则不会发生崩溃。

这只是 RxSwift 的皮毛 (20:31)

我展示了一些很酷的东西,通过将事件视为数组,你可以用 RxSwift 让代码更简单、更好用。我知道 MVVM 是一个非常庞大的项目,它可以将视图控制器从一个完整的个体变成一个个关联的组织。RxSwift 有一个相似的名为 RxCocoa 的仓库,可以有效地解决这个问题。基本上来说,它给所有的 Cocoa 类建立了扩展方法,从而可以让 UI 视图可以建立诸如 Rx-Text 或者名字文本框之类的东西。这样你就可以少写一点 subscribeNext 方法,从而在多个不同的视图点中将值和观察值之间建立关联。

跨平台 (22:49)

我们生活在一个有多个平台的世界。Rx 对我来说,最主要的吸引点就是 Rx 可以忽略每一个其他客户端 API 执行 IO 的方法。如果你在使用 Android 或者 JavaScript,你必须要学习如何异步管理这些不同的 IO 操作事件。Rx 是一个支持多平台的辅助库,你可以在多个热门语言中使用:.NET、JavaScript、Java,这三门是除了 Swift 之外最热门的三个语言了。你可以使用相同的操作、步骤和心态来编写代码。在所有的语言当中,Rx 看起来都是十分相似的。我们以一个日志功能为例,首先是 Swift:

func rx_login(username: String, password: String) -> Observable<Any> {
  return create({ (observer) -> Disposable in
    let postBody = [
      "username": username,
      "password": password
    ]
    let request = Alamofire.request(.POST, "login", parameters: postBody)
      .responseJSON(completionHandler: { (firedResponse) -> Void in
        if let value = firedResponse.result.value {
          observer.onNext(value)
          observer.onCompleted()
        } else if let error = firedResponse.result.error {
          observer.onError(error)
        }
      })
    return AnonymousDisposable{
      request.cancel()
    }
  })
}

rx_login 函数可以返回一个你所想要的观察者对象。下面是 Kotlin 版本:

fun rx_login(username: String, password: String): Observable<JSONObject> {
  return Observable.create ({ subscriber ->
    val body = JSONObject()
    body.put("username", username)
    body.put("password", password)
    val listener = Response.Listener<JSONObject>({ response ->
      subscriber.onNext(response);
      subscriber.onCompleted()
    })
    val errListener = Response.ErrorListener { err ->
      subscriber.onError(err)
    }
    val request = JsonObjectRequest(Request.Method.POST, "login", listener, errListener);
    this.requestQueue.add(request)
  });
}

看起来基本差不多,下面是 TypeScript 版本:

rx_login = (username: string, password: string) : Observable<any> => {
  return Observable.create(observer => {
    let body = {
      username: username,
      password: password
    };
    let request = $.ajax({
      method: 'POST',
      url: url,
      data: body,
      error: (err) => {
        observer.onError(err);
      },
      success: (data) => {
        observer.onNext(data);
        observer.onCompleted();
      }
    });
    return () => {
      request.abort()
    }
  });
}

不仔细看这些代码形式还真差不多。你可以对所有这类事件随意编写测试用例。你可以不必写所有的客户端代码或 UI 代码,因为它们都需要自身平台的支持,但是基于服务的类可以很容易地提取出相同的使用接收方式。几乎相同的原理无处不在。

问与答 (24:42)

问:您对 RxSwift 和 ReactiveCocoa 都有什么看法吗?

Max:我使用了三年的 Objective-C。ReactiveCocoa 是我早期的试用目标。当我换用 Swift 进行开发的时候,我安装了 ReactiveCocoa 的早期版本,使用起来让我感到很不愉快。我发现通过 Google 搜索我就可以很快地上手 RxSwift。对我个人而言,RxSwift 和 ReactiveCocoa 我都用。人们总会说这两个框架之间有着这样那样的差别,但是我从来没有听人说过 RxSwift 毁了某人的童年,也没听说过 ReactiveCocoa 让某人妻离子散。没有人曾经跟我谈论过这些差异。因此使用哪一个是你的自由。RxSwift 是 ReactiveX GitHub 仓库下的一个项目,因此如果如果对你来说阅读代码和学习很轻松的话,那么就直接使用 RxSwift 吧!如果你的公司只是关于 iOS 以及 Android 的话,那么就使用 ReactiveCocoa 就好了,就不要再考虑别的了。但是如果你需要准备三个平台的话,比如说有一个和 JS 应用一起使用的电子应用,最好是能够将这个应用放到 Spotify 上面,然后给三个平台分别复制服务然后创建一个监视器。你可以在一晚上完成这个任务。

问:关于自动完成功能速度慢的问题您有没有建议或者解决方案呢?

Max:我打字速度是很快的,比自动完成功能要快得多了。我大多数时候只在敲下点的时候才查看自动完成列表。如果你输入的速度过快,那么你很可能就无法得到自动完成提示。这是此时 Xcode 面临的实际问题。我在使用自动完成的时候并没有碰到问题,而通常我使用���动完成的方法是 flatMapmergecombineLatest

问:您提到了跨平台。它能在 Linux 上运行吗?

Max:我之所以提到跨平台的特性,是因为它本质上是一个拥有 API 特性的库。之所以这么说是因为你可以使用 Java 或者 TypeScript 来实现辅助库的功能,这个库本质上是独立运行的。

问:我注意到这个框架导入的是 Foundation 框架,我好奇的是,如果要摆脱这个基础库的依赖的话,或者替换这个基础库的时候,会不会非常难?

Max:我不清楚。我会具体问下别人这个问题,之后再来回复你。

问:如何对 RxSwift 进行调试呢?

Max:有一个带有闭包的调试函数的。RxSwift 有一个类似库是专门处理闭包的。这个库不会在正式产品中被使用。你实际使用之后你会发现它的功能真的十分好用。它会自行创建闭包调用,因此你无需对这个异步线程进行管理。因此你可以在主线程上面对堆栈进行跟踪。

问:我在想为什么您觉得专门挑选几个特殊的例子来介绍 RxSwift 是一个好主意呢?或者说为什么不全用上 RxSwift 或者 Rearctive 来创建一个完整的应用程序呢?

Max:很多对 RxSwift 进行贡献的人都说,你应当从一开始就在项目中使用 RxSwift。但是这很不现实,因此我决定有选择性地进行介绍。我不认为在场的绝大多数观众朋友们都能够有立马开始新项目的机会。你们大家很多时候是在处理着各种各样有五年或者六年历史的代码库,因此有选择性地进行介绍,决定是否使用就看大家是否喜欢了。

问:我已经用了很久的 ReactiveCocoa 了,其中一件 ReactiveCocoa 所要做的事就是他们想将所有事件都转为信号进行处理,这样你就可以将他们集中在一起。在 Objective-C 中他们使用 Selector 来实现此功能。你是否清楚在 RxSwift 中,它是如何使用 Swift 来处理委托回调方法的呢?

Max:是的,如果你看一下在 RxCocoa 中这一部分代码的话,它们会重新激活 (reactifying) 你的 UITableViewDelegatesUICollectionViewDelegates。它们会创建一个微妙的代理,这样你就可以通过闭包来开始接收事件,然后将其转换为观察者,以便创建属于你自己的观察者集,接着在委托层中接收信号。如果你查看 RxCocoa 库的话你会发现这个操作做得也是非常完美的。

About the content

This content has been published here with the express permission of the author.

Max Alexander

Max Alexander has been a contract iOS Developer for the last 2 years and recently has been the iOS Engineer for Hipmunk. He deeply loves reactive apps and always loves to evangelize teams to use RxJS, RxSwift, and RxJava whenever possible. He has a particular interest in Operational Transform and Offline-First application development On his leisure time Max contributes to GitHub Projects, develops games on Unreal Engine 4 and Unity.

4 design patterns for a RESTless mobile integration »

close