RxSwift的学习之路(二)——Subjects

最近比较忙,更新得有点慢,望谅解。

什么是Subject?

上一章我介绍了Observable——一个功能就像一条数据流的类。这一章的内容比较简单,SubjectObservable还是挺相似的,如果说Observable是专门用来被订阅获取数据的一个“被动”的类,那么比起ObservableSubject倒是占据了一点主动。

它可以作为一个被订阅者供给外部订阅,也可以作为一个观察者,接收事件,然后发出给订阅者。所以它要比Obsavable更加的灵活一点,按照我的理解,它应该是一种支持一边接收事件,一边接收订阅者的类。也就是说,我可以先创建一个Subject,不着急定制里面的事件(Event),然后先让它被某个订阅者订阅,再往里塞入事件,这样子也可以让订阅者作出响应。我觉得在上代码之前,徒有文字描述应该是相当抽象的了,但是在那之前,我还是得先介绍一下RxSwift里的四种Subjects。

  • PublishSubject:可以不需要初始来进行初始化(也就是可以为空),并且它只会向订阅者发送在订阅之后才接收到的元素。
  • BehaviorSubject:需要一个初始值来进行初始化,因为比起PublishSubject,它会为订阅者发送订阅前接收到的最后一个元素,当然,新事件也会发送。
  • ReplaySubject:初始化的时候要指定一个缓冲区的大小,而它会维持一个指定大小的数组来保存最近的元素,当有订阅者订阅了,它会首先向订阅者发送该缓冲区内的元素。然后当有新的元素加入,也会发送给订阅者。
  • Variable:这是一个不太一样的Subject,它实际上等同于包了一层BehaviorSubject,它里面有一个value属性等同于最近接收的一个元素,但是它本身不继承自Observable。需要调用它自带的asObservable()方法进行转化后才能被订阅。

注:我想你可能会好奇为什么我在介绍这四种subjects的时候老是提到元素这个词而不是事件这个词,元素实际上说的就是next事件中包含的元素。我会这么说的原因主要是上面提到的表现是subjects的主要表现,而它们都针对于next事件,一旦出现一个error或者completed的事件导致它们终结,它们的行为也会变得不一样。

PublishSubject——一个只会收发新元素的观察者

因为不知道怎么用中文称呼subject相关的类,但是它的功能有点像一个针对于Observable数据流的观察者,所以我就称它为观察者吧!
为了方便测试,我们先写下一些功能代码:

enum MyError: Error {
    case anError
}

func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
    print(label, event.element ?? event.error ?? event)
}

我前面提到,subject是一种可以一边收发事件,一边接收订阅的神奇物种,而PublishSubject是一种只对订阅者发出新元素的类,下面我将用代码来演示一下:

    let subject = PublishSubject<Int>()
    subject.onNext(1)

    subject.subscribe { print(label: "PS)", event: $0) }
        .addDisposableTo(disposeBag)
    
    subject.onNext(2)

/*
输出:
PS) 2
*/

从这段代码里就可以看出来,PublishSubject是只对订阅者发送新接收的信息的,而旧的消息则不会发出。
为了让这个原理更清晰,我们在上面的代码片继续添加如下代码:

    subject.subscribe { print(label: "PS2)", event: $0) }
        .addDisposableTo(disposeBag)
    
    subject.onNext(3)

/*
输出:
PS) 3
PS2) 3
*/

所以,对PublishSubject新增了一次订阅,它并不会把之前接收的元素1, 2, 3发送给第二次订阅。给出它的示意图:

PublishSubject

向上的箭头代表着一次订阅,乡下的箭头代表着subject对订阅者发送元素。

BehaviorSubject——一个会向每次订阅发出最近接收到的一个元素的观察者

BehaviorSubject是一个会向当前订阅发送最近接收的那个元素的功能类,因为有这样的要求,所以初始化一个BehaviorSubject一定要有一个初始值。 上代码:

    let subject = BehaviorSubject(value: 1)
    let disposeBag = DisposeBag()
    
    subject
        .subscribe { print(label: "BS)", event: $0) }            //BS) 1
        .addDisposableTo(disposeBag)
    
    subject.onNext(2)                                            //BS) 2
    
    subject
        .subscribe { print(label: "BS2)", event: $0) }           //BS2) 2
        .addDisposableTo(disposeBag)
    
    subject.onNext(3)                                            //BS) 3
                                                                 //BS2) 3
/*
输出结果:
BS) 1
BS) 2
BS2) 2
BS) 3
BS2) 3
*/

为了方便理解,我在每次订阅打印出信息的位置都额外添加了注释,以便让你看懂订阅后打印出对应信息的“位置”(或者说——顺序)。从上面就可以看出来,BehaviorSubject是会对订阅者发送最近的那次订阅的。给出示意图:

BehaviorSubject

ReplaySubject——会向每次订阅发送一系列最近元素的观察者

ReplaySubject会自带一个缓冲区,所以每次初始化的时候需要赋给它一个缓冲区大小。每次它接收到新的元素,都会先存放到自己的缓冲区里,按缓冲区大小来存放指定数量的元素(实际上BehaviorSubject就是一个缓冲区大小为1的ReplaySubject),然后在每次订阅发生的时候,则向订阅者发送缓冲区内的所有元素,然后才发送新接收到的元素。给出一个缓冲区大小为2的ReplaySubject的示意图:

ReplaySubject(bufferSize: 2)

给出样例代码以及输出:

    let subject = ReplaySubject<String>.create(bufferSize: 2)
    
    let disposeBag = DisposeBag()
    
    subject.onNext("1")
    subject.onNext("2")
    
    subject
        .subscribe { print(label: "RS)", event: $0) }      //RS) 1
        .addDisposableTo(disposeBag)                       //RS) 2
    
    subject.onNext("3")                                     //RS) 3
    
    subject
        .subscribe { print(label: "RS2)", event: $0) }    //RS2) 2
        .addDisposableTo(disposeBag)                      //RS2) 3

/*
输出结果:
RS) 1
RS) 2
RS) 3
RS2) 2
RS2) 3
*/

Variable——一个不太一样的Subject

Variable实质上是一个对BehaviorSubject还封装了一层的subject,它本身并不继承自Observable,所以它并不能被订阅,它有一个value属性用于访问它最近(也是最后)接收到的一个元素,所以对于Variable你甚至不需要订阅就能访问到它的数据流里的元素。而为了订阅到一个Variable实例的数据流,你得通过它的asObservable()方法去获取到它底层的BehaviorSubject才可以进行订阅。

并且,对于Variable而言,你无法像对其它subjects那样用onNext(element)onCompleted()或者onError(MyError.anError)来让它收到一个新的事件,甚至,它根本没法接收一个completed或者error事件。你只能通过Variable.value属性来为Variable添加一个新的元素。

还有,由于它是基于BehaviorSubject的封装,所以初始化一个Variable对象的时候也需要指定一个初始值。

    var variable = Variable("Initial value")
    
    let disposeBag = DisposeBag()
    
    variable.value = "New initial value"
    
    variable.asObservable()            //.asObservable() to access its underlying behavior subject
        .subscribe { print(label: "V)", event: $0) }
        .addDisposableTo(disposeBag)
    
    variable.value = "1"
    variable.asObservable()
        .subscribe { print(label: "V2)", event: $0) }
        .addDisposableTo(disposeBag)
    
    variable.value = "2"

/*
输出结果:
V) New Initial Value
V) 1
V2) 1
V) 2
V2) 2
*/

Subjects是如何面对completederror事件的?那dispose()呢?

文章也有点长度了,避免你忘记了我之前定义的一个枚举,我把它再定义一次:

enum MyError: Error {
    case anError
}

PublishSubject的情况

completed:
    let subject = PublishSubject<Int>()
    let disposeBag = DisposeBag()
    
    subject.onNext(1)
    
    subject.subscribe { print(label: "PS)", event: $0) }
        .addDisposableTo(disposeBag)
    
    subject.onNext(2)
    subject.onNext(3)
    
    subject.onCompleted()
    
    subject.subscribe { print(label: "PS2)", event: $0) }
        .addDisposableTo(disposeBag)
    
    subject.onNext(4)

/*
输出结果:
PS) 2
PS) 3
PS) completed
PS2) completed
*/

是的,就和Observable数据流的表现类似,当你向一个subject发送一个completed事件的时候,subject所拥有的数据流也会被标记终结,当你再往它推送新的元素,它也不会再发给订阅者。除此之外,它还将对新来的订阅者发送导致它终结的事件,这点对于所有的subjects都适用。而这也是为什么你会看到PS2)接收到了completed事件的原因(按照PublishSubject的功能,它不应该会发出一个订阅之前就接收到了的事件)。

个人观点,PublishSubject在接收到completed或者error的事件的时候,表现得就像BehaviorSubject一样。

error:

把上面那段代码中的subject.onCompleted()方法替换为:

subject.onError(MyError.anError)

效果和onCompleted()差不多,只不过会输出错误,就不多赘述了。

dispose:

subjects都是遵循于Disposable协议的,所以它们可以调用dispose()方法来析构自身,又或者用addToDisposeBag(DisposeBag())方法将它加入到垃圾袋里去将它析构。但是由于dispose()可以让subject立刻析构,便于我们看到析构后订阅的结果,所以我们就用它来进行实验吧!

把上面那段代码中的subject.onCompleted()方法替换为:

subject.dispose()

/*
输出结果:
PS) 2
PS) 3
PS2) Object `RxSwift.PublishSubject<Swift.Int>` was already disposed.
*/

看到了吗?所以在PublishSubject被回收后,它并不会对已订阅的对象发送消息,但是对于后来者,它则会返回一条对象已析构的错误。(当然了,通过.subscribe(onDisposed:{...})的方法可以让既订阅者对析构“事件”进行响应)。

BehaviorSubject的情况

它的表现和PublishSubject一致,不作赘述。

ReplaySubject的情况

completed:
    let subject = ReplaySubject<String>.create(bufferSize: 2)
    
    let disposeBag = DisposeBag()
    
    subject.onNext("1")
    subject.onNext("2")
    
    subject.onCompleted()
    
    subject
        .subscribe { print(label: "RS1)", event: $0) }
        .addDisposableTo(disposeBag)
    
    subject.onNext("3")
    
    subject
        .subscribe { print(label: "RS2)", event: $0) }
        .addDisposableTo(disposeBag)

/*
输出结果
RS1) 1
RS1) 2
RS1) completed
RS2) 1
RS2) 2
RS2) completed
*/

ReplaySubject和前两个subjects不太一样,它有一个缓冲区来装载最近接收的元素。在它接收到completed或者error事件的时候,他会向既订阅者发送该事件;对于新的订阅,它将先发出缓冲区的元素,接着再将导致数据流终结的对象也发送出去。它也有和前两个subjects一样的地方,那就是它被终结后,不会再接收新的元素。

error:

如前文所说,当你向一个ReplaySubject发送一个completederror事件,会导致它的数据流终结,此后,若再订阅它,ReplaySubject会先向订阅者发送缓冲区内的所有元素,再将导致它终结的completed或者error事件发送给订阅者。

dispose:

ReplaySubject在析构以后的表现和前两个subjects一样,它会通知既订阅者调用它们订阅了的onDisposed:{}“事件”里的闭包(假如订阅了的话)。而对于新来的订阅者,它不会像对待completederror那样发送缓冲区内的元素,而是仅仅抛出一个“对象已析构”的错误。

Variable的情况

对于Variable而言,它没办法终结,所以也无从讨论。但是它有它强大的地方,它既可以被订阅用来长期获取它的数据流里的数据,也可以被外界一次性的访问来满足某些需求(Variable.value既可以当setter塞入新元素,也可以当作getter访问数据流中最新的一个元素)。

总结

SubjectsObservable非常相似,导致我们有点难以区分它们。但是其实是有差别的,Observable的表现更像一条没有表情的等待着你处理的数据流,而它也是Subjects的根本所在,而Subjects则提供了你用不同的方式来操作一条数据流——Observable。这也是为什么我说Observable比较被动,而Subjects要偏主动一些的原因。简单来说,Observable的使用流程大概是接收-使用;而Subjects则是接收-采用自身的策略发出-使用

在这里我们还可以探寻一下它们的实用价值:

PublishSubject:总是发出最新的信息,你可以在你仅仅需要用到新数据的地方使用它,并且在你订阅的时候,如果没有新的信息,它将不会回调,在利用它来和界面绑定的时候,你得有一个默认的字段放在你界面上,以免界面上什么都没有。

BehaviorSubject:除了发出新的信息,还会首先发出最近接收到的最后一个元素。这里我们可以以微信(没有收广告费的)举个例子,譬如微信首页的tableview的cell里会显示最近的一条信息,而在这你就可以通过BehaviorSubject来订阅,从而用这条最近的信息作展示,而不需要等到新的信息到来,才做展示。

ReplaySubject:可是如果你现在订阅,却要获取最近的一批数据——譬如朋友圈,那该怎么办?显然只能依赖于ReplaySubject了吧?

这就是关于这一章我全部的心得体会了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容