精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

從響應(yīng)式編程到 Combine 實踐

原創(chuàng) 精選
開發(fā)
Combine 是響應(yīng)式編程的一種具體實現(xiàn),系統(tǒng)原生內(nèi)置與優(yōu)秀的實現(xiàn)讓它相較于其他響應(yīng)式框架有著諸多的優(yōu)勢,學(xué)習(xí)并掌握 Combine 是實踐響應(yīng)式編程的絕佳途徑,對日常開發(fā)也有諸多毗益。

作者 | ?何星

大約一年前,Resso 接入了 Combine,利用響應(yīng)式編程簡化了代碼邏輯,也積累了很多實踐經(jīng)驗。本文會從響應(yīng)式編程的基本思想并逐步深入介紹 Combine 的概念與最佳實踐, 希望能幫助更多的同學(xué)順利上手并實踐響應(yīng)式編程,少踩坑。

等等,Resso 是什么?Resso 來源于 Resonate(共鳴),是字節(jié)跳動推出的一個社交音樂流媒體平臺,專為下一代音樂發(fā)燒友設(shè)計,使他們能夠通過對音樂的熱愛來表達(dá)和與他人建立聯(lián)系。

書回正文,所謂的響應(yīng)式編程到底是什么呢?

熟悉 Combine 的同學(xué)可以直接跳到實踐建議部分。?

響應(yīng)式編程

維基百科對響應(yīng)式編程的定義是:

  • 在計算中,響應(yīng)式編程是一種面向數(shù)據(jù)流和變化傳播的聲明式編程范式。

雖然定義中每個字都認(rèn)識,但連起來卻十分費解。我們可以把定義中的內(nèi)容分開來理解,逐個擊破。首先,讓我們來看下聲明式編程。

聲明式編程

聲明式和指令式編程是常見的編程范式。在指令式編程中,開發(fā)者通過組合運(yùn)算、循環(huán)、條件等語句讓計算機(jī)執(zhí)行程序。聲明式與指令式正相反,如果說指令式像是告訴計算機(jī) How to do,而聲明式則是告訴計算機(jī) What to do。其實大家都接觸過聲明式編程,但在編碼時并不會意識到。各類 DSL 和函數(shù)式編程都屬于聲明式編程的范疇。

舉個例子,假設(shè)我們想要獲取一個整形數(shù)組里的所有奇數(shù)。按照指令式的邏輯,我們需要把過程拆解為一步一步的語句:

  • 遍歷數(shù)組中的所有元素。
  • 判斷是否為奇數(shù)。
  • 如果是的話,加入到結(jié)果中。繼續(xù)遍歷。
var results = [Int]()
for num in values {
if num %2 != 0 {
results.append(num)
}
}

如果按聲明式編程來,我們的想法可能是“過濾出所有奇數(shù)”,對應(yīng)的代碼就十分直觀:

var results = values.filter { $0 % 2 != 0 }

可見上述兩種編程方式有著明顯的區(qū)別:

  • 指令式編程:描述過程(How),計算機(jī)直接執(zhí)行并得結(jié)果。
  • 聲明式編程:描述結(jié)果(What),讓計算機(jī)為我們組織出具體過程,最后得到被描述的結(jié)果。

“面向數(shù)據(jù)流和變化傳播”

用說人話的方式解釋,面向數(shù)據(jù)流和變化傳播是響應(yīng)未來發(fā)生的事件流。

圖片

  1. 事件發(fā)布:某個操作發(fā)布了事件A?,事件A? 可以攜帶一個可選的數(shù)據(jù)B 。
  2. 操作變形:事件A? 與數(shù)據(jù)B? 經(jīng)過一個或多個的操作發(fā)生了變化,最終得到事件A'? 與數(shù)據(jù)B'。
  3. 訂閱使用:在消費端,有一個或多個訂閱者來消費處理后的A'? 和B',并進(jìn)一步驅(qū)動程序其他部分 (如 UI )

在這個流程中,無數(shù)的事件組成了事件流,訂閱者不斷接受到新的事件并作出響應(yīng)。

至此,我們對響應(yīng)式編程的定義有了初步的理解,即以聲明的方式響應(yīng)未來發(fā)生的事件流。在實際編碼中,很多優(yōu)秀的三方庫對這套機(jī)制進(jìn)一步抽象,為開發(fā)者提供了功能各異的接口。在 iOS 開發(fā)中,有三種主流的響應(yīng)式“流派“。

響應(yīng)式流派

  • ReactiveX:RxSwift
  • Reactive Streams:Combine
  • Reactive*:ReactiveCocoa / ReactiveSwift /ReactiveObjc

這三個流派分別是 ReactiveX、Reactive Streams 和 Reactive*。ReactiveX 接下來會詳細(xì)介紹。Reactive Stream 旨在定義一套非阻塞式異步事件流處理標(biāo)準(zhǔn),Combine 選擇了它作為實現(xiàn)的規(guī)范。以 ReactiveCocoa 為代表的 Reactive* 在 Objective-C 時代曾非常流行,但隨著 Swift 崛起,更多開發(fā)者選擇了 RxSwift 或 Combine,導(dǎo)致 Reactive* 整體熱度下降不少。

ReactiveX (Reactive Extension)

ReactiveX 最初是微軟在 .NET 上實現(xiàn)的一個響應(yīng)式的拓展。它的接口命名并不直觀,如 Observable (可觀測的) 和 Observer(觀測者)。ReactiveX 的優(yōu)勢在于創(chuàng)新地融入了許多函數(shù)式編程的概念,使得整個事件流的變形非常靈活。這個易用且強(qiáng)大的概念迅速被各個語言的開發(fā)者青睞,因此 ReactiveX 在很多語言都有對應(yīng)版本的實現(xiàn)(如 RxJS,RxJava,RxSwift),都非常流行。Resso 的 Android 團(tuán)隊就在重度使用 RxJava。

為何選擇 Combine

Combine 是 Apple 在 2019 年推出的一個類似 RxSwift 的異步事件處理框架。

通過對事件處理的操作進(jìn)行組合 (combine) ,來對異步事件進(jìn)行自定義處理 (這也正是 Combine 框架的名字的由來)。Combine 提供了一組聲明式的 Swift API,來處理隨時間變化的值。這些值可以代表用戶界面的事件,網(wǎng)絡(luò)的響應(yīng),計劃好的事件,或者很多其他類型的異步數(shù)據(jù)。

Resso iOS 團(tuán)隊也曾短暫嘗試過 RxSwift,但在仔細(xì)考察 Combine 后,發(fā)現(xiàn) Combine 無論是在性能、調(diào)試便捷程度上都優(yōu)于 RxSwift,此外還有內(nèi)置框架和 SwiftUI 官配的特殊優(yōu)勢,受其多方面優(yōu)勢的吸引,我們?nèi)媲袚Q到了 Combine。

Combine 的優(yōu)勢

相較于 RxSwift,Combine 有很多優(yōu)勢:

  • Apple 出品
  • 內(nèi)置在系統(tǒng)中,對 App 包體積無影響
  • 性能更好
  • Debug 更便捷
  • SwiftUI 官配

性能優(yōu)勢

Combine 的各項操作相較 RxSwift 有 30% 多的性能提升。

圖片

Reference: Combine vs. RxSwift Performance Benchmark Test Suite

Debug 優(yōu)勢

由于 Combine 是一方庫,在 Xcode 中開啟了 Show stack frames without debug symbols and between libraries 選項后,無效的堆棧可以大幅的減少,提升了 Debug 效率。

// 在 GlobalQueue 中接受并答應(yīng)出數(shù)組中的值
[1, 2, 3, 4].publisher
.receive(on: DispatchQueue.global())
.sink { value in
print(value)
}

圖片

Combine 接口

上文提到,Combine 的接口是基于 Reactive Streams Spec 實現(xiàn)的,Reactive Streams 中已經(jīng)定義好了 Publisher?, Subscriber,Subscription 等概念,Apple 在其上有一些微調(diào)。

具體到接口層面,Combine API 與 RxSwift API 比較類似,更精簡,熟悉 RxSwift 的開發(fā)者能無縫快速上手 Combine。Combine 中缺漏的接口可以通過其他已有接口組成替代,少部分操作符也有開源的第三方實現(xiàn),對生產(chǎn)環(huán)境的使用不會產(chǎn)生影響。

圖片

OpenCombine

細(xì)心的讀者可能有發(fā)現(xiàn) Debug 優(yōu)勢 的圖中出現(xiàn)了一個 OpenCombine。Combine 萬般好,但有一個致命的缺點:它要求的最低系統(tǒng)版本是 iOS 13,許多要維護(hù)兼容多個系統(tǒng)版本的 App 并不能使用。好在開源社區(qū)給力,實現(xiàn)了一份僅要求 iOS 9.0 的 Combine 開源實現(xiàn):OpenCombine。經(jīng)內(nèi)部測試,OpenCombine 的性能與 Combine 持平。OpenCombine 使用上與 Combine 差距很小,未來如果 App 的最低版本升級至 iOS 13 之后,從 OpenCombine 遷移到 Combine 的成本也很低,基本只有簡單的文本替換工作。公司內(nèi) Resso、剪映、醒圖、Lark 都有使用 OpenCombine。

Combine 基礎(chǔ)概念

上文提到,Combine 的概念基于 Reactive Streams。響應(yīng)式編程中的三個關(guān)鍵概念,事件發(fā)布/操作變形/訂閱使用,分別對應(yīng)到 Combine 中的 Publisher?, Operator? 與 Subscriber。

在簡化的模型中,首先有一個 Publisher?,經(jīng)過 Operater? 變換后被 Subscriber?消費。而在實際編碼中, Operator? 的來源可能是復(fù)數(shù)個 Publisher,Operator? 也可能會被多個 Publisher 訂閱,通常會形成一個非常復(fù)雜的圖。

圖片

Publisher

Publisher<Output, Failure: Error>

Publisher? 是事件產(chǎn)生的源頭。事件是 Combine 中非常重要的概念,可以分成兩類,一類攜帶了值(Value?),另外一類標(biāo)志了結(jié)束(Completion?)。結(jié)束的可以是正常完成(Finished?)或失敗(Failure)。

Events:
- Value:Output
- Completion
- Finished
- Failure(Error)

圖片

通常情況下, 一個 Publisher? 可以生成 N? 個事件后結(jié)束。需要注意的是,一個 Publisher?一旦發(fā)出了Completion(可以是正常完成或失敗),整個訂閱將結(jié)束,之后就不能發(fā)出任何事件了。

Apple 為官方基礎(chǔ)庫中的很多常用類提供了 Combine 拓展 Publisher,如 Timer, NotificationCenter, Array, URLSession, KVO 等。利用這些拓展我們可以快速組合出一個 Publisher,如:

// `cancellable` 是用于取消訂閱的 token,下文會詳細(xì)介紹
cancellable = URLSession.shared
// 生成一個 https://example.com 請求的 Publisher
.dataTaskPublisher(for: URL(string: "https://example.com")!)
// 將請求結(jié)果中的 Data 轉(zhuǎn)換為字符串,并忽略掉空結(jié)果,下面會詳細(xì)介紹 compactMap
.compactMap {
String(data: $0.data, encoding: .utf8)
}
// 在主線程接受后續(xù)的事件 (上面的 compactMap 發(fā)生在 URLSession 的線程中)
.receive(on: RunLoop.main)
// 對最終的結(jié)果(請求結(jié)果對應(yīng)的字符串)進(jìn)行消費
.sink { _ in
//
} receiveValue: { resultString in
self.textView.text = resultString
}

此外,還有一些特殊的 Publisher 也十分有用:

  • Future:只會產(chǎn)生一個事件,要么成功要么失敗,適用于大部分簡單回調(diào)場景
  • Just?:對值的簡單封裝,如Just(1)
  • @Published?:下文會詳細(xì)介紹 在大部分情況下,使用這些特殊的Publisher? 以及下文介紹的Subject 可以靈活組合出滿足需要的事件源。極少的情況下,需要實現(xiàn)自定義的 Publisher ,可以看這篇文章。

Subscriber

Subscriber<Input, Failure: Error>

Subsriber? 作為事件的訂閱端,它的定義與 Publisher? 對應(yīng),Publisher? 中的 Output?對應(yīng)Subscriber? 的 Input?。常用的 Subscriber? 有 Sink? 和 Assign。

Sink? 直接對事件流進(jìn)行訂閱使用,可以對 Value? 和 completion 分別進(jìn)行處理。

Sink 這個單詞在初次看到會令人非常費解。這個術(shù)語可來源于網(wǎng)絡(luò)流中的匯點(Sink),我們也可以理解為 The stream goes down the sink。

// 從數(shù)組生成一個 Publisher
cancellable = [1, 2, 3, 4, 5].publisher
.sink { completion in
// 處理事件流結(jié)束
} receiveValue: { value in
// 打印會每個值,會依次打印出 1, 2, 3, 4, 5
print(value)
}

Assign? 是一個特化版的 Sink? ,支持通過 KeyPath 直接進(jìn)行賦值。

let textLabel = UILabel()
cancellable = [1, 2, 3].publisher
// 將 數(shù)字 轉(zhuǎn)換為 字符串,并忽略掉 nil ,下面會詳細(xì)介紹這個 Operator
.compactMap { String($0) }
.assign(to: \.text, on: textLabel)

需要留意的是,如果用 assign? 對 self? 進(jìn)行賦值,可能會形成隱式的循環(huán)引用,這種情況需要改用 sink? 與 weak self 手動進(jìn)行賦值。

Cancellable & AnyCancellable

細(xì)心的讀者可能發(fā)現(xiàn)了上面出現(xiàn)了一個 cancellable?。每一個訂閱都會生成一個 AnyCancellable 對象,用于控制訂閱的生命周期。通過這個對象,我們可以取消訂閱。當(dāng)這個對象被釋放時,訂閱也會被取消。

// 取消訂閱
cancellable.cancel()

需要注意的是,每一個訂閱我們都需要持有這個 cancellable,否則整個訂閱會立即被取消并結(jié)束掉。

Subscription

Publisher? 和 Subscriber? 之間是通過 Subscription 建立連接。理解整個訂閱過程對后續(xù)深入使用 Combine 非常有幫助。

圖片

圖片來自《SwiftUI 和 Combine 編程》

Combine 的訂閱過程其實是一個拉取模型。

  1. Subscriber? 發(fā)起一個訂閱,告訴Publisher 我需要一個訂閱。
  2. Publisher? 返回一個訂閱實體(Subscription)。
  3. Subscriber? 通過這個Subscription? 去請求固定數(shù)量(Demand)的數(shù)據(jù)。
  4. Publisher? 根據(jù)Demand? 返回事件。單次的Demand? 發(fā)布完成后,如果Subscriber?繼續(xù)請求事件,Publisher 會繼續(xù)發(fā)布。
  5. 繼續(xù)發(fā)布流程。
  6. 當(dāng)Subscriber? 請求的事件全部發(fā)布完成后,Publisher? 會發(fā)送一個Completion。

Subject

Subject<Output, Failure: Error>

Subject? 是一類特殊的 Publisher?,我們可以通過方法調(diào)用(如 send())手動向事件流中注入新的事件。

private let isPlayingPodcastSubject = CurrentValueSubject<Bool, Never>(false)
// 向 isPlayingPodcastPublisher 注入一個新的事件,它的值是 true
isPlayingPodcastSubject.send(true)

Combine 提供了兩個常用的 Subject:PassthroughSubject? 與 CurrentValueSubject。

  • PassthroughSubject?:透傳事件,不會持有最新的Output
  • CurrentValueSubject?:除了傳遞事件之外,會持有最新的Output

@Published

對于剛接觸 Combine 的同學(xué)來說,最困擾的問題莫過于難以找到可以直接使用的事件源。Combine 提供了一個 Property Wrapper @Pubilshed? 可以快速封裝一個變量得到一個 Publisher。

// 聲明變量
class Alarm {
@Published
public var countDown = 0
}

let alarm = Alarm()

// 訂閱變化
let cancellable = alarm.$countDown // Published<Int>.Publisher
.sink { print($0) }

// 修改 countDown,上面 sink 的閉包會觸發(fā)
alarm.countDown += 1

上面比較有趣的是 $countDown? 訪問到的一個 Publisher?,這其實是一個語法糖,$? 訪問到其實是 countDown? 的 projectedValue?,正是對應(yīng)的 Publisher。

@propertyWrapper public struct Published<Value> {
// ...
/// The property for which this instance exposes a publisher
///
/// The ``Published/projectedValue` is the property accessed with the `$` operator
public var projectedValue: Published<Value>.Publisher { mutating get set }
}

@Published 非常適合在模塊內(nèi)對事件進(jìn)行封裝,類型擦除后提供外部進(jìn)行訂閱消費。

實際實踐中,對于已有的代碼邏輯,使用 @Published? 可以在不改動其他代碼快速讓屬性得到 Publisher 的能力。而新編寫的代碼,如果不會發(fā)生錯誤且需要使用到當(dāng)前的 Value,@Published? 也是很好的選擇,除此之外則需要按需考慮使用 PassthroughSubject? 或 CurrentValueSubject。

Operator

現(xiàn)實編碼中,Publisher? 攜帶的數(shù)據(jù)類型可能并不滿足我們的需求,這時需要使用 Operator 對數(shù)據(jù)進(jìn)行變換。Combine 自帶了非常豐富的 Operator,接下來會針對其中常用的幾個進(jìn)行介紹。

map, filter, reduce

熟悉函數(shù)式編程的同學(xué)對這幾個 Operator 應(yīng)該非常熟悉。它們的作用與在數(shù)組上的效果非常相似,只不過這次是在異步的事件流中。

例如,對于 map 來說,他會對每個事件中的值進(jìn)行變換:

圖片

[1, 2, 3].publisher
.map { $0 * 10 }
.sink { value in
// 將會答應(yīng)出 10, 20, 30
print(value)
}

filter? 也類似,會對每個事件用閉包里的條件進(jìn)行過濾。reduce 則會對每個事件的值進(jìn)行計算,最后將計算結(jié)果傳遞給下游。

compactMap

對于 Value 是 Optional? 的事件流,可以使用 compactMap 得到一個 Value 為非空類型的 Publisher。

// Publiser<Int?, Never> -> Publisher<Int, Never>
cancellable = [1, nil, 2, 3].publisher
.compactMap { $0 }
.map { $0 * 10 }
.sink { print($0) }

flatMap

flatMap 是一個特殊的操作符,它將每一個的事件轉(zhuǎn)換為一個事件流并合并在一起。舉例來說,當(dāng)用戶在搜索框輸入文本時,我們可以訂閱文本的變化,并針對每一個文本生成對應(yīng)的搜索請求 Publisher,并將所有 Publisher 的事件匯聚在一起進(jìn)行消費。

圖片

其他常見的 Operator 還有 zip?, combineLatest 等。

實踐建議

類型擦除

Combine 中的 Publisher? 在經(jīng)過各種 Operator 變換之后會得到一個多層泛型嵌套類型:

URLSession.shared.dataTaskPublisher(for: URL(string: "https://resso.com")!)
.map { $0.data }
.decode(type: String.self, decoder: JSONDecoder())
// 這個 publisher 的類型是 Publishers.Decode<Publishers.Map<URLSession.DataTaskPublisher, JSONDecoder. Input>, String, JSONDecoder>

如果在 Publisher? 創(chuàng)建變形完成后立即訂閱消費,這并不會帶來任何問題。但一旦我們需要把這個 Publisher? 提供給外部使用時,復(fù)雜的類型會暴露過多內(nèi)部實現(xiàn)細(xì)節(jié),同時也會讓函數(shù)/變量的定義非常臃腫。Combine 提供了一個特殊的操作符 erasedToAnyPublisher,讓我們可以擦除掉具體類型:

// 生成一個類型擦除后的請求。函數(shù)的返回值更簡潔
func requestRessoAPI() -> AnyPublisher<String, Error> {
let request = URLSession.shared.dataTaskPublisher(for: URL(string: "https://resso.com")!)
.map { $0.data }
.decode(type: String.self, decoder: JSONDecoder())
// Publishers.Decode<Publishers.Map<URLSession.DataTaskPublisher, JSONDecoder. Input>, String, JSONDecoder>
// to
// AnyPublisher<String, Error>
return request.eraseToAnyPublisher()
}

// 在模塊外,不用關(guān)心 `requestRessoAPI()` 返回的具體類型,直接進(jìn)行消費
cancellable = requestRessoAPI().sink { _ in

} receiveValue: {
print($0)
}

通過類型擦除,最終暴露給外部的是一個簡單的 AnyPublisher<String, Error>。

Debugging

響應(yīng)式編程寫起來非常的行云流水,但 Debug 起來就相對沒有那么愉快了。對此,Combine 也提供了幾個 Operator 幫助開發(fā)者 Debug。

Debug Operator

print 和 handleEvents

print 可以打印出整個訂閱過程從開始到結(jié)束的 Subscription 變化與所有值,例如:

cancellable = [1, 2, 3].publisher
.receive(on: DispatchQueue.global())
// 使用 `Array Publisher` 作為所有打印內(nèi)容的前綴
.print ( "Array Publisher")
.sink { _ in }

可以得到:

Array Publisher: receive subscription: (ReceiveOn)
Array Publisher: request unlimited
Array Publisher: receive cancel
Array Publisher: receive value: (1)
Array Publisher: receive value: (2)
Array Publisher: receive value: (3)
Array Publisher: receive finished

在一些情況下,我們只對所有變化中的部分事件感興趣,這時候可以用 handleEvents? 對部分事件進(jìn)行打印。類似的還有 breakpoint,可以在事件發(fā)生時觸發(fā)斷點。

畫圖法

到了萬策盡的地步,用圖像理清思路也是很好的方法。對于單個 Operator,可以在 RxMarble 找到對應(yīng) Operator 確認(rèn)理解是否正確。對于復(fù)雜的訂閱,可以畫圖確認(rèn)事件流的傳遞是否符合預(yù)期。

let greetings = PassthroughSubject<String, Never>()
let names = PassthroughSubject<String, Never>()
let years = PassthroughSubject<Int, Never>()
// CombineLatest 會選用兩個事件流中最新的值生成新的事件流
let greetingNames = Publishers.CombineLatest(greetings, names)
.map {"\($1) \($0)" }
let wholeSentence = Publishers.CombineLatest(greetingNames, years)
.map { ")($0), \($1)" }
.sink { print($0) }

greetings.send("Hello")
names.send("Combine")
years.send(2022)

圖片

常見錯誤

立即開始的 Just 和 Future

對于大部分的 Publisher?來說,它們在訂閱后才會開始生產(chǎn)事件,但也有一些例外。Just? 和 Future 在初始化完成后會立即執(zhí)行閉包生產(chǎn)事件,這可能會讓一些耗時長的操作在不符合預(yù)期的時機(jī)提前開始,也可能會讓第一個訂閱錯過一些太早開始的事件。

func makeMyPublisher () -> AnyPublisher<Int, Never> {
Just(calculateTimeConsumingResult())
.eraseToAnyPublisher()
}

一個可行的解法是在這類 Publisher? 外封裝一層 Defferred,讓它在接收到訂閱之后再開始執(zhí)行內(nèi)部的閉包。

func makeMyFuture2( ) -> AnyPublisher<Int, Never> {
Deferred {
return Just(calculateTimeConsumingResult())
}.eraseToAnyPublisher()
}

發(fā)生錯誤導(dǎo)致 Subscription 意外結(jié)束

func requestingAPI() -> AnyPublisher<String, Error> {
return URLSession.shared
.dataTaskPublisher(for: URL(string: "https://resso.com")!)
.map { $0.data }
.decode(type: String.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}

cancellable = NotificationCenter.default
.publisher(for: UserCenter.userStateChanged)
.flatMap({ _ in
return requestingAPI()
})
.sink { completion in

} receiveValue: { value in
textLabel.text = value
}

上面的代碼中將用戶狀態(tài)的通知轉(zhuǎn)化成了一個網(wǎng)絡(luò)請求,并將請求結(jié)果更新到一個 Label 上。需要留意的是,一旦某次網(wǎng)絡(luò)請求發(fā)生錯誤,整個訂閱會被結(jié)束掉,后續(xù)新的通知并不會被轉(zhuǎn)化為請求。

cancellable = NotificationCenter.default
.publisher(for: UserCenter.userStateChanged)
.flatMap { value in
return requestingAPI().materialize()
}
.sink { text in
titleLabel.text = text
}

解決這個問題的方式有很多,上面使用 materialize? 將事件從 Publisher<Output, MyError>? 轉(zhuǎn)換為 Publisher<Event<Output, MyError>, Never> 從而避免了錯誤發(fā)生。

Combine 官方并沒有實現(xiàn) materialize ,CombineExt 提供了開源的實現(xiàn)。

Combine In Resso

Resso 在很多場景使用到了 Combine,其中最經(jīng)典的例子莫過于音效功能中多個屬性的獲取邏輯。音效需要使用專輯封面,專輯主題色以及歌曲對應(yīng)的特效配置來驅(qū)動音效播放。這三個屬性分別需要使用三個網(wǎng)絡(luò)請求來獲取,如果使用 iOS 中經(jīng)典的閉包回調(diào)來編寫這部分邏輯,那嵌套三個閉包,陷入回調(diào)地獄,更別提其中的錯誤分支很有可能遺漏。

func startEffectNormal() {
// 1. 獲取歌曲封面
WebImageManager.shared.requestImage(trackCoverURL) { result in
switch result {
case .success(let image):
// 2. 獲取特效配置
fetchVisualEffectConfig(for: trackID) { result in
switch result {
case .success(let path):
// 3. 獲取封面主題色
fetchAlbumColor(trackID: trackID) { result in
switch result {
case .success(let albumColor):
self.startEffect(coverImage: coverImage, effectConfig: effectConfig, coverColor: coverColor)
case .failure:
// 處理獲取封面顏色錯誤
break
}
}
case .failure(let error):
// 處理獲取特效配置錯誤
break
}
}
case .failure(let error):
// 處理下載圖片錯誤
break
}
}
}

使用 Combine,我們可以把三個請求封裝成單獨的 Publisher?,再通過 combineLatest 將三個結(jié)果合并在一起進(jìn)行使用:

func startEffect() {
// 獲取歌曲封面的 Publisher
cancellable = fetchTrackCoverImagePublisher(for: trackCoverURL)
// 并與 獲取特效配置的 Publisher 和 獲取專輯主題色的 Publisher 中的最新結(jié)果組成新的 Publisher
.combineLatest(fetchVisualEffectPathPublisher(for: trackID), fetchAlbumColorPublisher(trackID: trackID))
// 對最終的結(jié)果進(jìn)行使用
.sink { completion in
if case .failure(let error) = completion {
// 對錯誤進(jìn)行處理
}
} receiveValue: { (coverImage, effectConfig, coverColor) in
self.startEffect(coverImage: coverImage, effectConfig: effectConfig, coverColor: coverColor)
}
}

這樣的實現(xiàn)方式帶來了很多好處:

  1. 代碼結(jié)構(gòu)更緊湊,可讀性更好
  2. 錯誤處理更集中,不易遺漏
  3. 可維護(hù)性更好,后續(xù)如果需要新的請求,只需繼續(xù) combine 新的 Publisher 即可

此外,Resso 也對自己的網(wǎng)絡(luò)庫實現(xiàn)了 Combine 拓展,方便更多的同學(xué)開始使用 Combine:

func fetchSomeResource() -> RestfulClient<SomeResponse>.DataTaskPublisher{
let request = SomeRequest()
return RestfulClient<SomeResponse>(request: request)
.dataTaskPublisher
}

總結(jié)

一言以蔽之,響應(yīng)式編程的核心在于用聲明的方式響應(yīng)未來發(fā)生的事件流。在日常的開發(fā)中,合理地使用響應(yīng)式編程可以大幅簡化代碼邏輯,但在不適宜的場景(甚至是所有場景)濫用則會讓同事 ??。常見的多重嵌套回調(diào)、自定義的通知都是非常適合切入使用的場景。

Combine 是響應(yīng)式編程的一種具體實現(xiàn),系統(tǒng)原生內(nèi)置與優(yōu)秀的實現(xiàn)讓它相較于其他響應(yīng)式框架有著諸多的優(yōu)勢,學(xué)習(xí)并掌握 Combine 是實踐響應(yīng)式編程的絕佳途徑,對日常開發(fā)也有諸多毗益。

責(zé)任編輯:未麗燕 來源: 字節(jié)跳動技術(shù)團(tuán)隊
相關(guān)推薦

2021-08-12 18:48:31

響應(yīng)式編程Bio

2023-04-06 09:42:00

LispHTMLQwit

2022-03-09 23:02:30

Java編程處理模型

2022-09-01 08:00:00

響應(yīng)式編程集成

2025-05-06 01:14:00

系統(tǒng)編程響應(yīng)式

2021-01-25 05:38:04

設(shè)計原理VueSubject

2021-07-14 13:12:51

2022-07-15 08:16:56

Stream函數(shù)式編程

2022-05-28 11:00:57

安全編碼安全代碼應(yīng)用安全

2024-04-11 14:00:28

2010-06-22 13:32:26

函數(shù)式編程JavaScript

2024-01-11 11:25:22

2024-07-03 10:09:29

2017-03-14 19:18:56

AndroidGradle實踐

2023-10-04 00:43:46

推導(dǎo)式Python

2025-08-29 08:28:13

2023-07-12 08:16:54

JVM工具包Vert.x

2021-07-28 20:13:04

響應(yīng)式編程

2022-10-25 08:05:12

Kotlin響應(yīng)式編程

2022-08-25 11:00:19

編程系統(tǒng)
點贊
收藏

51CTO技術(shù)棧公眾號

日韩av在线免费播放| 一区二区欧美精品| 国产精品福利小视频| 成人黄色短视频| 91国内精品| 日本久久一区二区三区| 国产精品美女在线播放| 人妻中文字幕一区| 日韩在线一区二区三区| 欧美老女人性视频| 中国毛片在线观看| 欧美久久亚洲| 欧美亚洲免费在线一区| 激情六月天婷婷| www.在线播放| 99精品在线免费| 成人一区二区电影| www.久久久久久久| 你懂的国产精品| 亚洲人成在线观看| 日本少妇一级片| 国产极品嫩模在线观看91精品| 亚洲线精品一区二区三区| 天天爽天天狠久久久| 日韩一级免费毛片| 激情欧美一区二区| 国产精品狠色婷| 国产一级做a爰片在线看免费| 人人狠狠综合久久亚洲婷婷| 欧美精品一区二区三区蜜臀| 久久久久久久久久一区| 三上悠亚激情av一区二区三区| 一二三区精品视频| 伊甸园精品99久久久久久| 亚洲色图狠狠干| 国产成人综合网站| 91九色国产视频| 少妇无套内谢久久久久| 国产毛片久久| 午夜精品一区二区三区视频免费看| 天堂网中文在线观看| 欧美猛男同性videos| 亚洲黄色免费三级| 年下总裁被打光屁股sp | 午夜精品久久久久久久久久久久久| 欧美巨胸大乳hitomi| 欧美男gay| 亚洲美腿欧美激情另类| 精品人妻一区二区免费| 欧美经典一区| 日韩精品一区二区三区老鸭窝 | 成人性生交大片免费观看网站| 亚洲一区二区黄色| 91黄色在线看| 97在线超碰| 亚洲大片在线观看| 老太脱裤让老头玩ⅹxxxx| 国精一区二区三区| 亚洲永久精品大片| 隔壁人妻偷人bd中字| 大桥未久在线播放| 精品久久久久久久久久久久久久 | 3d性欧美动漫精品xxxx软件| 岛国精品视频在线播放| 男人日女人bb视频| 日韩精品三区| 欧美精品在线视频| 亚洲一区二区三区三州| 亚洲视频三区| 日韩成人在线视频| 人人妻人人澡人人爽| 成人午夜国产| 久久综合伊人77777蜜臀| 国产高清在线免费观看| 精品999日本| 欧美亚洲国产视频小说| 中文字幕一区二区三区四区欧美| 人妖欧美一区二区| 99一区二区三区| 欧美一区二区三区黄片| 国产丝袜欧美中文另类| 一区二区三区免费看| 污视频网站在线免费| 午夜精品久久久久久久久久久| 国产精品动漫网站| 涩涩涩久久久成人精品| 精品国产一区二区国模嫣然| 少妇特黄一区二区三区| 99久久亚洲精品| 国内精品久久久久影院 日本资源 国内精品久久久久伊人av | 免费成人高清视频| 在线观看 中文字幕| 视频一区在线播放| 99r国产精品视频| 欧美伦理影视网| 亚洲天堂免费在线观看视频| 99久久国产综合精品五月天喷水| 亚洲www免费| 欧美一卡二卡三卡| 日本黄色网址大全| 91高清一区| 欧美亚洲另类制服自拍| 国产又大又长又粗| 91色九色蝌蚪| 亚洲精品偷拍视频| 欧美人与性动交xxⅹxx| 日韩三级视频中文字幕| 熟女俱乐部一区二区| 国产精品激情| 国产日韩欧美电影在线观看| 少妇一级淫片免费看| 中文字幕一区二区三区色视频| 成人黄色av片| 亚洲天堂av资源在线观看| 国产一区二区三区网站| 日韩在线视频免费播放| 国产一区二区三区精品欧美日韩一区二区三区 | 丰满熟女人妻一区二区三| 成人免费黄色在线| 日日噜噜噜夜夜爽爽| 美女网站视频一区| 日韩电影网在线| 久草国产在线观看| 韩国精品久久久| 先锋影音日韩| 欧美无毛视频| 亚洲精品自拍偷拍| 午夜偷拍福利视频| 国产一区福利在线| 正在播放91九色| 国产成人精品一区二区三区在线| 亚洲欧美激情四射在线日| 久久午夜无码鲁丝片午夜精品| 国产呦萝稀缺另类资源| 综合网五月天| 欧美视频在线视频精品| 亚洲午夜女主播在线直播| av黄色在线看| 91色视频在线| 国产精品宾馆在线精品酒店| 精品国产午夜肉伦伦影院| 久久人人爽人人爽人人片av高请 | 四虎成人在线观看| 91网站在线播放| 高清在线观看免费| 色88888久久久久久影院| 久久久久国产视频| 黄色一级大片在线免费看国产| 亚洲人妖av一区二区| 色偷偷中文字幕| 艳女tv在线观看国产一区| 91免费看片在线| 欧美人体视频xxxxx| 欧美大片日本大片免费观看| 久久精品波多野结衣| 岛国av在线一区| 国产美女主播在线播放| 卡通动漫精品一区二区三区| 欧美又大又粗又长| 国产在线视频网址| 欧美日韩大陆在线| 欧美手机在线观看| 国产激情视频一区二区在线观看| 国产成人永久免费视频| 国产精品传媒| 国产999在线| 99视频在线观看地址| 555夜色666亚洲国产免| 国产乱国产乱老熟300| 成人免费视频一区| 成人在线免费在线观看| 欧美日韩国产一区二区三区不卡| 国产精品香蕉国产| 国产剧情在线| 亚洲电影天堂av| 三级网站在线播放| 国产精品久久久久久久久免费相片 | 超碰97免费在线| 亚洲久久久久久久久久久| 夜夜躁很很躁日日躁麻豆| 亚洲精品乱码久久久久久久久 | 亚洲.国产.中文慕字在线| 毛茸茸多毛bbb毛多视频| 日韩精品亚洲专区| 在线观看免费黄色片| 欧美国产不卡| 成人av在线网址| 狠狠操一区二区三区| 在线观看日韩av| 成人小说亚洲一区二区三区| 日本韩国一区二区三区视频| 日本妇女毛茸茸| 久久久99久久精品欧美| 亚洲AV无码久久精品国产一区| 91久久黄色| 一区二区三区观看| 婷婷综合福利| 69174成人网| 97欧美成人| 久久人人97超碰精品888| 免费网站免费进入在线| 日韩精品视频免费专区在线播放 | 日韩av最新在线观看| 人妻中文字幕一区二区三区| 午夜精品久久久久| 五月婷婷综合激情网| wwwwxxxxx欧美| 欧美xxxx黑人| 六月丁香婷婷色狠狠久久| 久久久一本二本三本| 性xxxx欧美老肥妇牲乱| 欧美综合激情| 精品国产乱子伦一区二区| 91精品久久久久久久久| 神马电影网我不卡| 欧美国产高跟鞋裸体秀xxxhd| 自拍视频在线| 在线观看成人黄色| 青青草在线免费视频| 亚洲成人a**站| 国产成a人亚洲精v品无码| 欧美日精品一区视频| 日本中文字幕久久| 天天色天天操综合| 欧美另类视频在线观看| ...xxx性欧美| 国产色无码精品视频国产| 国产日本一区二区| 精品人妻一区二区三区香蕉 | 男人的天堂亚洲| 久久精品视频16| 激情久久久久久| 国产一级做a爰片久久毛片男| 香蕉久久网站| 日本老太婆做爰视频| 久久一区91| 中国人体摄影一区二区三区| 色777狠狠狠综合伊人| 先锋影音日韩| 99re66热这里只有精品8| 亚洲一区二区精品在线| 欧美3p视频| 伊人情人网综合| 综合久久一区| 男人天堂新网址| 欧美三区美女| 精品少妇人妻av免费久久洗澡| 激情视频一区| koreanbj精品视频一区| av成人天堂| 精品人妻一区二区三区四区在线| 免费视频久久| 精品久久久久久久无码 | 精品国产1区2区| youjizz在线视频| 欧美自拍丝袜亚洲| 中文av免费观看| 91精品国产综合久久久蜜臀粉嫩 | 亚洲电影一区| 国产女主播一区二区| 日本韩国欧美超级黄在线观看| 久久99国产精品| 欧美理论视频| 法国空姐在线观看免费| 国自产拍偷拍福利精品免费一| 成人免费观看cn| 天堂影院一区二区| 99日在线视频| proumb性欧美在线观看| 丰满少妇一区二区| 国产精品欧美一区喷水| 国产少妇在线观看| 婷婷亚洲久悠悠色悠在线播放| 麻豆精品久久久久久久99蜜桃| 欧美亚洲一区二区在线观看| 国产高清第一页| 亚洲毛片在线观看.| 在线国产情侣| 国内精品免费午夜毛片| 精品日本视频| 18成人在线| 精品一区毛片| 亚洲五码在线观看视频| 国产精品久久久一区二区| 天天干天天操天天玩| 成人激情免费电影网址| 免费一级黄色录像| 亚洲最大成人综合| 国产天堂第一区| 亚洲白拍色综合图区| 91精彩视频在线观看| 欧美精品福利在线| 91大神在线观看线路一区| 国产欧美一区二区三区不卡高清| 欧美日韩中文一区二区| 国产一级爱c视频| 极品尤物av久久免费看| 欧美大片免费播放器| 亚洲欧美偷拍三级| 免费看污视频的网站| 精品国产亚洲一区二区三区在线观看| 国产女人在线视频| 91精品国产一区| 中文字幕日韩亚洲| 欧美日韩系列| 红桃视频欧美| 亚洲免费999| 国产无一区二区| 日本学生初尝黑人巨免费视频| 欧美久久久久久久久中文字幕| 四虎成人免费在线| 欧美精品videos| 亚洲伦理网站| 婷婷五月色综合| 免费亚洲一区| av网站免费在线播放| 亚洲激情图片一区| 精品国产青草久久久久96| 日韩成人小视频| 超碰在线公开| 国产精品中出一区二区三区| 羞羞答答成人影院www| 日本在线一二三区| 国产亚洲va综合人人澡精品| 九九热在线视频播放| 亚洲国产精品大全| 手机av在线播放| 成人黄在线观看| 久久国产精品亚洲人一区二区三区 | 久久网一区二区| 欧美一级片免费看| 精品视频在线一区二区| 国产一区二区香蕉| 国产精品久久久久蜜臀| 99re6在线观看| 国产精品色噜噜| 中文字幕av网站| 国产亚洲人成网站在线观看| 三级成人在线| 亚洲成人自拍视频| 美女国产一区二区三区| 国产黄色片在线| 3d动漫精品啪啪一区二区竹菊| 秋霞影院午夜丰满少妇在线视频| 国产日韩在线精品av| 久久激情电影| 国产九九九视频| 一区二区三区在线视频免费| 亚洲精品一级片| 韩国日本不卡在线| 欧美激情极品| 国产极品美女高潮无套久久久| 国产视频亚洲色图| 97精品人妻一区二区三区香蕉| 日韩中文字在线| 国产一区二区三区亚洲综合| 日本大片免费看| 99精品久久99久久久久| 手机看片久久久| 中文字幕av一区中文字幕天堂| 日韩一区二区三区四区五区 | 天天人人精品| 狠狠色2019综合网| 国产一级大片在线观看| 日韩电影免费观看在线观看| 黄色精品视频| 国产经典久久久| 91毛片在线观看| 亚洲男人天堂网址| 久久久国产在线视频| 999久久久精品一区二区| www.浪潮av.com| 国产精品美女一区二区三区| 国产aⅴ一区二区三区| 91国自产精品中文字幕亚洲| 精品久久影视| 国产成人精品综合久久久久99 | 国产精品美女www爽爽爽| 99久久免费国产精精品| 97精品国产aⅴ7777| 欧美日韩老妇| 五月天丁香社区| 91国在线观看| 丝袜国产在线| 日韩欧美手机在线| 国产成人精品影视| 日本a级c片免费看三区| 不卡av在线播放| 国产精品三级| 中文字幕18页| 欧美日韩三级一区| √8天堂资源地址中文在线| 亚洲精品国产一区| aaa国产一区| 一区二区日韩在线观看| 欧美中文字幕在线播放| 午夜激情久久| 免费网站在线高清观看| 精品国产一区二区三区不卡| 日韩专区视频网站|