利用Swift實(shí)現(xiàn)一個(gè)響應(yīng)式編程庫
前言
整個(gè)2017年我完全使用 Swift 進(jìn)行開發(fā)了。使用 Swift 進(jìn)行開發(fā)是一個(gè)很愉快的體驗(yàn),我已經(jīng)完全不想再去碰 OC 了。最近想做一個(gè)響應(yīng)式編程的庫,所以就把它拿來分享一下。
在缺乏好的資源的情況下,學(xué)習(xí)響應(yīng)式編程成為痛苦。我開始學(xué)的時(shí)候,做死地找各種教程。結(jié)果發(fā)現(xiàn)有用的只是極少部分,而且這少部分也只是表面上的東西,對(duì)于整個(gè)體系結(jié)構(gòu)的理解也起不了多大的作用。
Reactive Programing
說到響應(yīng)式編程,ReactiveCocoa 和 RxSwift 可以說是目前 iOS 開發(fā)中最優(yōu)秀的第三方開源庫了。今天咱們不聊 ReactiveCocoa 和 RxSwif,咱們自己來寫一個(gè)響應(yīng)式編程庫。如果你對(duì)觀察者模式很熟悉的話,那么響應(yīng)式編程就很容易理解了。
響應(yīng)式編程是一種面向數(shù)據(jù)流和變化傳播的編程范式。
比如用戶輸入、單擊事件、變量值等都可以看做一個(gè)流,你可以觀察這個(gè)流,并基于這個(gè)流做一些操作?!氨O(jiān)聽”流的行為叫做訂閱。響應(yīng)式就是基于這種想法。
廢話不多說,擼起袖子開干。
我們以一個(gè)獲取用戶信息的網(wǎng)絡(luò)請(qǐng)求為例:
func fetchUser(with id: Int, completion: @escaping ((User) -> Void)) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) { let user = User(name: "jewelz") completion(user) } }
上面是我們通常的做法,在請(qǐng)求方法里傳入一個(gè)回調(diào)函數(shù),在回調(diào)里拿到結(jié)果。在響應(yīng)式里面,我們監(jiān)聽請(qǐng)求,當(dāng)請(qǐng)求完成時(shí),觀察者得到更新。
func fetchUser(with id: Int) -> Signal {}
發(fā)送網(wǎng)絡(luò)請(qǐng)求就可以這樣:
fetchUser(with: "12345").subscribe({ })
在完成 Signal 之前, 需要定義訂閱后返回的數(shù)據(jù)結(jié)構(gòu),這里我只關(guān)心成功和失敗兩種狀態(tài)的數(shù)據(jù),所以可以這樣寫:
enum Result { case success(Value) case error(Error) }
現(xiàn)在可以開始實(shí)現(xiàn)我們的 Signal 了:
final class Signal { fileprivate typealias Subscriber = (Result) -> Void fileprivate var subscribers: [Subscriber] = [] func send(_ result: Result) { for subscriber in subscribers { subscriber(result) } } func subscribe(_ subscriber: @escaping (Result) -> Void) { subscribers.append(subscriber) } }
寫個(gè)小例子測(cè)試一下:
let signal = Signal() signal.subscribe { result in print(result) } signal.send(.success(100)) signal.send(.success(200)) // Print success(100) success(200)
我們的 Signal 已經(jīng)可以正常工作了,不過還有很多改進(jìn)的空間,我們可以使用一個(gè)工廠方法來創(chuàng)建一個(gè) Signal, 同時(shí)將 send變?yōu)樗接械模?/p>
static func empty() -> ((Result) -> Void, Signal) { let signal = Signal() return (signal.send, signal) } fileprivate func send(_ result: Result) { ... }
現(xiàn)在我們需要這樣使用 Signal 了:
let (sink, signal) = Signal.empty() signal.subscribe { result in print(result) } sink(.success(100)) sink(.success(200))
接著我們可以給 UITextField 綁定一個(gè) Signal,只需要在 Extension 中給 UITextField添加一個(gè)計(jì)算屬性 :
extension UITextField { var signal: Signal { let (sink, signal) = Signal.empty() let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in sink(.success(str)) } signal.objects.append(observer) return signal } }
上面代碼中的 observer 是一個(gè)局部變量,在 signal調(diào)用完后,就會(huì)被銷毀,所以需要在 Signal 中保存該對(duì)象,可以給 Signal 添加一個(gè)數(shù)組,用來保存需要延長(zhǎng)生命周期的對(duì)象。 KeyValueObserver 是對(duì) KVO 的簡(jiǎn)單封裝,其實(shí)現(xiàn)如下:
final class KeyValueObserver: NSObject { private let object: NSObject private let keyPath: String private let callback: (T) -> Void init(object: NSObject, keyPath: String, callback: @escaping (T) -> Void) { self.object = object self.keyPath = keyPath self.callback = callback super.init() object.addObserver(self, forKeyPath: keyPath, options: [.new], context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath, keyPath == self.keyPath, let value = change?[.newKey] as? T else { return } callback(value) } deinit { object.removeObserver(self, forKeyPath: keyPath) } }
現(xiàn)在就可以使用textField.signal.subscribe({})
來觀察 UITextField 內(nèi)容的改變了。
在 Playground 寫個(gè) VC 測(cè)試一下:
class VC { let textField = UITextField() var signal: Signal? func viewDidLoad() { signal = textField.signal signal?.subscribe({ result in print(result) }) textField.text = "1234567" } deinit { print("Removing vc") } } var vc: VC? = VC() vc?.viewDidLoad() vc = nil // Print success("1234567") Removing vc
Reference Cycles
我在上面的 Signal 中,添加了 deinit方法:
deinit { print("Removing Signal") }
最后發(fā)現(xiàn) Signal 的析構(gòu)方法并沒有執(zhí)行,也就是說上面的代碼中出現(xiàn)了循環(huán)引用,其實(shí)仔細(xì)分析上面 UITextField 的拓展中 signal的實(shí)現(xiàn)就能發(fā)現(xiàn)問題出在哪兒了。
let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in sink(.success(str)) }
在 KeyValueObserver 的回調(diào)中,調(diào)用了 sink()
方法,而 sink 方法其實(shí)就是 signal.send(_:)
方法,這里在閉包中捕獲了signal 變量,于是就形成了循環(huán)引用。這里只要使用 weak 就能解決。修改下的代碼是這樣的:
static func empty() -> ((Result) -> Void, Signal) { let signal = Signal() return ({[weak signal] value in signal?.send(value)}, signal) }
再次運(yùn)行, Signal 的析構(gòu)方法就能執(zhí)行了。
上面就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的響應(yīng)式編程的庫了。不過這里還存在很多問題,比如我們應(yīng)該在適當(dāng)?shù)臅r(shí)機(jī)移除觀察者,現(xiàn)在我們的觀察者被添加在 subscribers 數(shù)組中,這樣就不知道該移除哪一個(gè)觀察者,所以我們將數(shù)字替換成字典,用 UUID 作為 key :
fileprivate typealias Token = UUID fileprivate var subscribers: [Token: Subscriber] = [:]
我們可以模仿 RxSwift 中的 Disposable 用來移除觀察者,實(shí)現(xiàn)代碼如下:
final class Disposable { private let dispose: () -> Void static func create(_ dispose: @escaping () -> Void) -> Disposable { return Disposable(dispose) } init(_ dispose: @escaping () -> Void) { self.dispose = dispose } deinit { dispose() } }
原來的 subscribe(_:) 返回一個(gè) Disposable 就可以了:
func subscribe(_ subscriber: @escaping (Result) -> Void) -> Disposable { let token = UUID() subscribers[token] = subscriber return Disposable.create { self.subscribers[token] = nil } }
這樣我們只要在適當(dāng)?shù)臅r(shí)機(jī)銷毀 Disposable 就可以移除觀察者了。
作為一個(gè)響應(yīng)式編程庫都會(huì)有 map, flatMap, filter, reduce 等方法,所以我們的庫也不能少,我們可以簡(jiǎn)單的實(shí)現(xiàn)幾個(gè)。
map
map 比較簡(jiǎn)單,就是將一個(gè) 返回值為包裝值的函數(shù) 作用于一個(gè)包裝(Wrapped)值的過程, 這里的包裝值可以理解為可以包含其他值的一種結(jié)構(gòu),例如 Swift 中的數(shù)組,可選類型都是包裝值。它們都有重載的 map, flatMap等函數(shù)。以數(shù)組為例,我們經(jīng)常這樣使用:
let images = ["1", "2", "3"].map{ UIImage(named: $0) }
現(xiàn)在來實(shí)現(xiàn)我們的 map 函數(shù):
func map(_ transform: @escaping (Value) -> T) -> Signal { let (sink, signal) = Signal.empty() let dispose = subscribe { (result) in sink(result.map(transform)) } signal.objects.append(dispose) return signal }
我同時(shí)給 Result 也實(shí)現(xiàn)了 map 函數(shù):
extension Result { func map(_ transform: @escaping (Value) -> T) -> Result { switch self { case .success(let value): return .success(transform(value)) case .error(let error): return .error(error) } } } // Test let (sink, intSignal) = Signal.empty() intSignal .map{ String($0)} .subscribe { result in print(result) } sink(.success(100)) // Print success("100")
flatMap
flatMap 和 map 很相似,但也有一些不同,以可選型為例,Swif t是這樣定義 map 和 flatMap 的:
public func map(_ transform: (Wrapped) throws -> U) rethrows -> U? public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U?
flatMap 和 map 的不同主要體現(xiàn)在 transform 函數(shù)的返回值不同。map 接受的函數(shù)返回值類型是 U類型,而 flatMap 接受的函數(shù)返回值類型是 U?類型。例如對(duì)于一個(gè)可選值,可以這樣調(diào)用:
let aString: String? = "¥99.9" let price = aString.flatMap{ Float($0)} // Price is nil
我們這里 flatMap 和 Swift 中數(shù)組以及可選型中的 flatMap 保持了一致。
所以我們的 flatMap 應(yīng)該是這樣定義:flatMap(_ transform: @escaping (Value) -> Signal) -> Signal。
理解了 flatMap 和 map 的不同,實(shí)現(xiàn)起來也就很簡(jiǎn)單了:
func flatMap(_ transform: @escaping (Value) -> Signal) -> Signal { let (sink, signal) = Signal.empty() var _dispose: Disposable? let dispose = subscribe { (result) in switch result { case .success(let value): let new = transform(value) _dispose = new.subscribe({ _result in sink(_result) }) case .error(let error): sink(.error(error)) } } if _dispose != nil { signal.objects.append(_dispose!) } signal.objects.append(dispose) return signal }
現(xiàn)在我們可以模擬一個(gè)網(wǎng)絡(luò)請(qǐng)求來測(cè)試 flatMap:
func users() -> Signal { let (sink, signal) = Signal.empty() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) { let users = Array(1...10).map{ User(id: String(describing: $0)) } sink(.success(users)) } return signal } func userDetail(with id: String) -> Signal { let (sink, signal) = Signal.empty() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) { sink(.success(User(id: id, name: "jewelz"))) } return signal } let dispose = users() .flatMap { return self.userDetail(with: $0.first!.id) } .subscribe { result in print(result) } disposes.append(dispose) // Print: success(ReactivePrograming.User(name: Optional("jewelz"), id: "1"))
通過使用 flatMap ,我們可以很簡(jiǎn)單的將一個(gè) Signal 轉(zhuǎn)換為另一個(gè) Signal , 這在我們處理多個(gè)請(qǐng)求嵌套時(shí)就會(huì)很方便了。
寫在最后
上面通過100 多行的代碼就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的響應(yīng)式編程庫。不過對(duì)于一個(gè)庫來說,以上的內(nèi)容還遠(yuǎn)遠(yuǎn)不夠?,F(xiàn)在的 Signal 還不具有原子性,要作為一個(gè)實(shí)際可用的庫,應(yīng)該是線程安的。還有我們對(duì) Disposable 的處理也不夠優(yōu)雅,可以模仿 RxSwift 中 DisposeBag 的做法。上面這些問題可以留給讀者自己去思考了。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Swift 5.1 之類型轉(zhuǎn)換與模式匹配的教程詳解
這篇文章主要介紹了Swift 5.1 之類型轉(zhuǎn)換與模式匹配的相關(guān)知識(shí),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05Swift重構(gòu)自定義空等運(yùn)算符 “??=” 實(shí)例
這篇文章主要為大家介紹了Swift重構(gòu)自定義空等運(yùn)算符 “??=” 實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03Swift中內(nèi)置的集合類型學(xué)習(xí)筆記
Swift中自帶數(shù)組、set、字典三大集合類型,這里將學(xué)習(xí)過程中的基礎(chǔ)的Swift中內(nèi)置的集合類型學(xué)習(xí)筆記進(jìn)行整理,需要的朋友可以參考下2016-06-06Swift實(shí)現(xiàn)復(fù)數(shù)計(jì)算器
這篇文章主要為大家詳細(xì)介紹了Swift實(shí)現(xiàn)復(fù)數(shù)計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01