RxSwift_操作符_map、flatmap、flatMapLatest

map操作符将源Observable的每个元素,通过提供的方法转换,然后返回含有转换后元素的Observable

#案例1:

Observable.of(1,2,3)
    .map{
        $0 * 10
    }
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

/// 打印结果:
/// 10
/// 20
/// 30

flatMap 操作符会对源Observable的每一个元素应用一个转换方法,将他们转换成Observable,然后将这些Observable的元素合并之后再发送出来,即将其降维成一个Observable序列

#案例2:

/// 如果Observable中的元素也是Observable
Observable.of(1,2,3)
    .map { value in
        return Observable.just(value * 10)
    }
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

/// 打印结果:
RxSwift.(unknown context at $10dc3e738).Just<Swift.Int>
RxSwift.(unknown context at $10dc3e738).Just<Swift.Int>
RxSwift.(unknown context at $10dc3e738).Just<Swift.Int>

/// 通过flatMap降维,获取内部的元素值
Observable.of(1,2,3)
    .map { value in
        return Observable.just(value * 10)
    }
    .flatMap({
        return $0
    })
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

    }

/// 打印结果:
/// 10
/// 20
/// 30
#案例3:

# 这种写法也是可以的,为什么? 
# 请看下方讲解
Observable.of(1,2,3)
    .flatMap { (value) -> Observable<Int> in
        return Observable.just(value * 10)
    }
    .subscribe(onNext:{
        print($0)
   })
   .disposed(by: bag)

/// 打印结果:
/// 10
/// 20
/// 30
/// 源码解析:
/// 首先selector函数型参数返回的是一个可观察对象,因为根据Source.Element可知,
/// 闭包函数返回的是一个可观察对象,如果此闭包函数返回的不是一个可观察对象则会报错
///
/// 第二:
/// selector闭包函数入参Self.Element,可以是普通的数据类型,也可以是可观察对象
/// 如果是普通数据类型,那么就必须要在闭包函数中将其包装为可观察对象
/// 如果其本身就是可观察对象,那么可以直接返回,不需要再包装
public func flatMap<Source>(_ selector: @escaping (Self.Element) throws -> Source) 
-> RxSwift.Observable<Source.Element> 
where Source : RxSwift.ObservableConvertibleType

flatMapLatest: 当源序列有新的事件发生的时候,flatMapLatest会自动取消上一个是事件的订阅,转到新的事件的订阅上面,而flatMap则会订阅全部

#案例4:

# 这种情况,flatMapLatest 与 flatMap 没什么不同,不能体现两者的区别
Observable.of(1,2,3)
    .flatMapLatest { (value) -> Observable<Int> in  
      ///  Observable.just 是一次性的可观察序列,发生完Event后,就直接Complete结束
      /// 不能再发送第二个次的Event
      /// 所以这不能很好的体现flatMapLatest的主要功能:取消上一次的事件订阅事件
        return Observable.just(value * 10)
    }
    /// subscribe订阅的不是Observable.of(1,2,3)创建的可观察序列
    /// 而是flatMap转变后的可观察序列
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

/// 打印结果:
/// 10
/// 20
/// 30

flatMapLatest 与 flatMap 使用区别案例

struct Player {
    var score : BehaviorRelay<Int>    
}

flatMap的实现

let aPlayer = Player(score: BehaviorRelay(value: 1))
let bPlayer = Player(score: BehaviorRelay(value: 2))

let players = PublishSubject<Player>()

players.asObserver()
    .flatMap { (player) -> Observable<Int> in
        player.score.asObservable()
    }
    /// subscribe 订阅的不是players
    /// 而是players事件中包含的每一个player.score
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

/// players 发送Event,对象是aPlayer,因此aPlayer被订阅了
players.onNext(aPlayer)
/// aPlayer对象,发出事件,因为被订阅了,所以能接收到并打印
aPlayer.score.accept(3)

/// players 再次发出Event,对象是bPlayer,因此bPlayer也被订阅了
/// 但是aPlayer的订阅并没有被取消
players.onNext(bPlayer)

/// aPlayer对象再发出事件,依然能被接收和打印
aPlayer.score.accept(4)

/// 打印结果:
1
3
2
4

flatMapLatest的实现

let aPlayer = Player(score: BehaviorRelay(value: 1))
let bPlayer = Player(score: BehaviorRelay(value: 2))

let players = PublishSubject<Player>()

players.asObserver()
    .flatMapLatest { (player) -> Observable<Int> in
        player.score.asObservable()
    }
    .subscribe(onNext:{
        print($0)
    })
    .disposed(by: bag)

players.onNext(aPlayer)
aPlayer.score.accept(3)

/// players发起一个新的事件,会对bPlayer订阅,并且同时取消上一次Event发送过来的aPlayer对象
players.onNext(bPlayer)

/// aPlayer被取消订阅了,所以没有打印
aPlayer.score.accept(4)

/// 打印结果:
1
3
2

总结:如果flatMap接收事件Event包裹的元素也是一个Observable,并且不希望保存上一次的订阅,则可以使用flatMapLatest

BehaviorRelay 、Subject的使用,可以参考这篇文章

勘误:map、flatMap、flatMapLatest最终返回的都是一个可观察序列(比如Observable、Driver),
不同的是map闭合函数返回的值,map函数会自动包装一层可观察序列
flatMap、flatMapLatest闭合函数返回的必须是一个可观察序列
与map作用最大的区别是如果源可观察序列携带的也是可观察序列元素,那么flatMap、flatMapLatest就可以将闭合函数入参的可观察序列直接返回,这样达到一个“降维”的效果,订阅flatMap、flatMapLatest转换过后,就能直接获取到可观察序列中的元素(非可观察序列),如有不明的请仔细查看案例2中的代码


项目实操

看了很多篇文章,一直说flapMap、flapMapLatest很好用,但是一直都没说要怎么用,那到底要怎么用了?(学以致用很重要)

首先要知道的是flapMapflapMapLatest返回的是一个可观察序列Observable,一个可观察序列,相等是一个“输出源”,可以进行订阅监听,根据不同的输出做不同的处理

继续以登录界面为例:一个手机号,一个验证码,一个登录按钮,实现点击登录按钮,发起网络请求

# 未改进的代码:有一个最大的问题,请看代码注释

let telObservable = telInput.rx.text.orEmpty
let codeObservable = codeInput.rx.text.orEmpty
let tapObservable = loginBtn.rx.tap

let userNameAndCode = Observable<(String,String)>.combineLatest(telObservable, codeObservable) {
    return ($0,$1)
}

/// 直接订阅登录按钮的点击事件,在订阅中直接发起请求
/// 问题:不能优雅的将网络请求结果反馈到UI层中,将UI刷新。
/// 可能会通过多创建几个Subject或可观察序列的方式来实现反馈,这样很多余,并且把问题复杂化
loginAction
    .withLatestFrom(userNameAndCode)
    .subscribe(onNext:{ (tel,code) in
        /// 发起网络请求
    })
    .disposed(by: disposebag)

# 改进后的代码
let telObservable = telInput.rx.text.orEmpty
let codeObservable = codeInput.rx.text.orEmpty
let tapObservable = loginBtn.rx.tap

let userNameAndCode = Observable<(String,String)>.combineLatest(telObservable, codeObservable) {
    return ($0,$1)
}


/// 优点:
/// 第一:在flatMapLatest序列转换的时候,就发起网络请求
/// 第二:flatMapLatest序列转换后的结果,必定是一个Observable可观察序列,将这个序列抛到UI层订阅,就能直接的将网络请求结果反馈到UI中刷新处理

let loginResutl = loginAction.
        .withLatestFrom(userNameAndCode)
        .flatMapLatest { (arg0) -> Observable<String> in
            /// 发起网络请求,然后将结果包裹成Observable再发送出去
            et (a, b) = arg0
            return Observable.just("\(a)\(b)")
        }

提示:
(1)Observable 与 Driver 对于 连着使用withLatestFrom、flatMapLatest操作符是不太一样的请看下方
(2)对于:不会产生error事件、在主线程监听、并且共享状态变化的极力推荐使用Driver替换Observable

let telObservable = telInput.rx.text.orEmpty.asDriver()
let codeObservable = codeInput.rx.text.orEmpty.asDriver()
let tapObservable = loginBtn.rx.tap.asDriver()

let userNameAndCode = Driver<(String,String)>.combineLatest(telObservable, codeObservable) {
    return ($0,$1)
}

let loginResutl = loginAction
    .withLatestFrom(userNameAndCode)
    /// Driver 中的flatMapLatest闭合函数要求返回的是 :RxCocoa.SharedSequence,也就是Driver对象
    /// Observable中flatMapLatest闭合函数要求非的是 :RxSwift.Observable
    .flatMapLatest { (arg0) -> Driver<String> in
        return Observable<String>.just("网络请求结果").asDriver(onErrorJustReturn: "网络异常")
    }

PS:对于完整的登录功能Demo,要本人再多几次的review之后一定会出一篇文章,敬请期待!!


2021年3月19日更新
flatMap 与 map 的重要不同,flatMap可以起到一个降维的作用(flatMap与map的配合使用可以达到不错的效果)

Observable.of(1,2,3)
    .map { value in
        return Observable.just(value * 10)
    }
    .flatMap({ value -> Observable<Int> in
        print("flatMap输出:\(value)")
        return value
    })
    .map({(value) -> Int in
        print("map输出:\(value)")
        return value
    })
    .subscribe(onNext:{ value in
    })
    .disposed(by: bag)

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

推荐阅读更多精彩内容