自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從響應(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ā)者通過組合運算、循環(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 選項后,無效的堆??梢源蠓臏p少,提升了 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?)或失?。‵ailure)。

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)式編程集成

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

2017-03-14 19:18:56

AndroidGradle實踐

2024-01-11 11:25:22

2024-07-03 10:09:29

2023-10-04 00:43:46

推導(dǎo)式Python

2021-07-28 20:13:04

響應(yīng)式編程

2023-07-12 08:16:54

JVM工具包Vert.x

2022-08-25 11:00:19

編程系統(tǒng)

2022-10-25 08:05:12

Kotlin響應(yīng)式編程

2021-12-12 18:15:06

Python并發(fā)編程

2020-08-18 10:20:50

Java 編程開發(fā)
點贊
收藏

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