Combine之Operator(Mixing datas 数据合并)

1,714 阅读4分钟

github.com/agelessman/…

这一小节中提到的数据合并本质上是多个publishers的合并,按照不同的合并方式可以分为3类:

  • combineLatest
  • merge
  • zip

combineLatest

image.png

一图胜千言,上边这张图已经表明了combineLatest的最核心的思想:

  • 它合并了2个publisher
  • 输出的数据是成组的,数据结构为元组

数据成组即是优点也是缺点,优点就不多说了,缺点是,如果数据不能成组就不会输出数据。正常代码如下:

let firstPublisher = PassthroughSubject<Int, MyCustomError>()
let secondPublisher = PassthroughSubject<String, MyCustomError>()

firstPublisher
    .combineLatest(secondPublisher)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

firstPublisher.send(1)
secondPublisher.send("a")
firstPublisher.send(2)

打印结果:

someValue: (1, "a")
someValue: (2, "a")

我们先让second publisher不输出数据看看输出是什么?

...
firstPublisher.send(1)
/// secondPublisher.send("a")
firstPublisher.send(2)

实际运行上边代码,并不会有任何输出,这就说明,如果2个publisher中,有任何一个没有数据,pipline就不会输出数据。

那么,如果second publisher在发送数据后,又发送了.finished事件会如何呢?我们稍微修改下代码:

...
firstPublisher.send(1)
secondPublisher.send("a")
secondPublisher.send(completion: Subscribers.Completion.finished)
firstPublisher.send(2)

打印结果:

someValue: (1, "a")
someValue: (2, "a")

可以看出,即使second publisher发送了.finished事件,整个pipline仍然会继续执行,在合并数据的时候,会使用publisher的最后一个数据。

更进一步,如果second publisher发送.failure事件会怎样呢?修改代码:

enum MyCustomError: Error {
    case custom
}
...
firstPublisher.send(1)
secondPublisher.send("a")
secondPublisher.send(completion: Subscribers.Completion.failure(MyCustomError.custom))
firstPublisher.send(2)
someValue: (1, "a")
结束了
Error...

可以看出,pipline对error是十分敏感的,一旦发现错误,就立刻终止pipline。

大家明白了吗?总结一下:combineLatest的核心思想是把2个publisher的数据进行合并,正常情况下,2个publisher中任何一个产生新的数据,就会把数据合并后输出,遇到错误pipline立刻终止。

上边只讲解了combineLatest合并2个publisher,它还可以合并3个和4个publisher,用法都是一样的,看下边两张图片就明白了:

image.png


image.png

merge

image.png

merge也主要用于合并publisers,最多能够合并8个,最少2个,但是它有自己的特点,combineLatest会把每个publiser的latest数据组合成一个元组,而merge只要接受到新数据就输出。

merge比较适用于合并多个无序的publishers场景。至于某个publisher发送.finished或者.failure事件的逻辑跟combineLatest一样。

cancellables = Set<AnyCancellable>()

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
let pub3 = PassthroughSubject<Int, Never>()
let pub4 = PassthroughSubject<Int, Never>()
let pub5 = PassthroughSubject<Int, Never>()
let pub6 = PassthroughSubject<Int, Never>()
let pub7 = PassthroughSubject<Int, Never>()
let pub8 = PassthroughSubject<Int, Never>()

pub1
    .merge(with: pub2, pub3,
           pub4, pub5,
           pub6, pub7, pub8)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

pub1.send(1)
pub2.send(2)
pub3.send(3)
pub4.send(4)
pub5.send(5)
pub6.send(6)
pub7.send(7)
pub8.send(8)

zip

image.png

zip和combineLatest非常相似,默认情况下会返回成组的数据,但他们最大的不同在于zip不会使用latest值,它使用的是新值。

观察上图,当pub2返回了数据4后,pipline中返回了(1, 4),说明了数据流中使用的数据是这2个publisher中从未使用过的新值,这些新值会遵循先入先出的规则。这就是zip最大的特点。

这个特点在某些场景下会非常有用,比方说需要等待2个异步请求完毕后,拿到2个数据继续执行其他的任务,这种需求就非常适合zip。

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

pub1
    .zip(pub2)
    .sink(receiveCompletion: { completion in
        print("结束了")
        switch completion {
        case .finished:
            print("完成")
        case .failure(let error):
            print("错误:\(error.localizedDescription)")
        }
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

pub1.send(1)
pub1.send(2)
pub1.send(3)
pub2.send(4)
pub2.send(5)

zip最多可以合并4个publishers,原理都是相同的,我们在这里就略过不讲了,zip还有一个特性,它提供了一个闭包参数,允许我们对数据进行转换,我们先看看示意图:

image.png

可以看出,pipline的输出类型为String,我们可以使用闭包自由地对数据进行转换,这在平时的开发中非常有用。代码如下:

let numberPublisher = PassthroughSubject<Int, Never>()
let emojiPublisher = PassthroughSubject<String, Never>()

numberPublisher
    .zip(emojiPublisher) { number, emoji -> String in
        String(repeating: emoji, count: number)
    }
    .sink(receiveCompletion: { completion in
        ...
    }, receiveValue: { someValue in
        print("someValue: \(someValue)")
    })
    .store(in: &cancellables)

numberPublisher.send(2)
emojiPublisher.send("😂")

打印结果:

someValue: 😂😂

**总结一下,zip的特点是使用各个publisher的新值,而且能够进行数据映射。在真实开发中,当有多个异步任务需要合并的情景下可以考虑combineLatest, merge, zip。