用Swift寫一個響應式編程庫

MartinHenne 7年前發布 | 13K 次閱讀 Swift RxSwift

2017年又快過去了,忙了一年感覺沒啥收獲,感覺是不是應該寫點啥,想了好久沒想出要寫什么。下半年因為工作的原因,狗狗也沒養了,吉他上也積滿了灰塵,興致勃勃的學習素描,到現在也沒畫出了啥,博客也很久沒更新了。想想感覺更新一下博客吧。

整個2017年我完全使用 Swift 進行開發了。使用 Swift 進行開發是一個很愉快的體驗,我已經完全不想再去碰 OC 了。最近想做一個響應式編程的庫,所以就把它拿來分享一下。

Reactive Programing

說到響應式編程,ReactiveCocoa 和 RxSwift 可以說是目前 iOS 開發中最優秀的第三方開源庫了。今天咱們不聊 ReactiveCocoa 和 RxSwif,咱們自己來寫一個響應式編程庫。如果你對觀察者模式很熟悉的話,那么響應式編程就很容易理解了。

響應式編程是一種面向數據流和變化傳播的編程范式。

比如用戶輸入、單擊事件、變量值等都可以看做一個流,你可以觀察這個流,并基于這個流做一些操作。“監聽”流的行為叫做訂閱。響應式就是基于這種想法。

廢話不多說,擼起袖子開干。

我們以一個獲取用戶信息的網絡請求為例:

func fetchUser(with id: Int, completion: @escaping ((User) -> Void)) {
     DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {
         let user = User(name: "jewelz")
         completion(user)
     }
}

上面是我們通常的做法,在請求方法里傳入一個回調函數,在回調里拿到結果。在響應式里面,我們監聽請求,當請求完成時,觀察者得到更新。

func fetchUser(with id: Int) -> Signal {}

發送網絡請求就可以這樣:

fetchUser(with: "12345").subscribe({

})

在完成 Signal 之前, 需要定義訂閱后返回的數據結構,這里我只關心成功和失敗兩種狀態的數據,所以可以這樣寫:

enum Result {
    case success(Value)
    case error(Error)
}

現在可以開始實現我們的 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)
    }
}

寫個小例子測試一下:

let signal = Signal()
signal.subscribe { result in
    print(result)
}
signal.send(.success(100))
signal.send(.success(200))
// Print
success(100)
success(200)

我們的 Signal 已經可以正常工作了,不過還有很多改進的空間,我們可以使用一個工廠方法來創建一個 Signal, 同時將 send變為私有的:

static func empty() -> ((Result) -> Void, Signal) {
     let signal = Signal()
     return (signal.send, signal)
}
fileprivate func send(_ result: Result) { ... }

現在我們需要這樣使用 Signal 了:

let (sink, signal) = Signal.empty()
signal.subscribe { result in
    print(result)
}
sink(.success(100))
sink(.success(200))

接著我們可以給 UITextField 綁定一個 Signal,只需要在 Extension 中給 UITextField添加一個計算屬性 :

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 是一個局部變量,在 signal調用完后,就會被銷毀,所以需要在 Signal 中保存該對象,可以給 Signal 添加一個數組,用來保存需要延長生命周期的對象。 KeyValueObserver 是對 KVO 的簡單封裝,其實現如下:

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)
    }
}

現在就可以使用textField.signal.subscribe({}) 來觀察 UITextField 內容的改變了。

在 Playground 寫個 VC 測試一下:

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")
}

最后發現 Signal 的析構方法并沒有執行,也就是說上面的代碼中出現了循環引用,其實仔細分析上面 UITextField 的拓展中 signal的實現就能發現問題出在哪兒了。

let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in
    sink(.success(str))
}

在 KeyValueObserver 的回調中,調用了 sink()方法,而 sink 方法其實就是 signal.send(_:)方法,這里在閉包中捕獲了signal 變量,于是就形成了循環引用。這里只要使用 weak 就能解決。修改下的代碼是這樣的:

static func empty() -> ((Result) -> Void, Signal) {
     let signal = Signal()
     return ({[weak signal] value in signal?.send(value)}, signal)
}

再次運行, Signal 的析構方法就能執行了。

上面就實現了一個簡單的響應式編程的庫了。不過這里還存在很多問題,比如我們應該在適當的時機移除觀察者,現在我們的觀察者被添加在 subscribers 數組中,這樣就不知道該移除哪一個觀察者,所以我們將數字替換成字典,用 UUID 作為 key :

fileprivate typealias Token = UUID
fileprivate var subscribers: [Token: Subscriber] = [:]

我們可以模仿 RxSwift 中的 Disposable 用來移除觀察者,實現代碼如下:

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(_:) 返回一個 Disposable 就可以了:

func subscribe(_ subscriber: @escaping (Result) -> Void) -> Disposable {
     let token = UUID()
     subscribers[token] = subscriber
      return Disposable.create {
          self.subscribers[token] = nil
      }   
 }

這樣我們只要在適當的時機銷毀 Disposable 就可以移除觀察者了。

作為一個響應式編程庫都會有 map, flatMap, filter, reduce 等方法,所以我們的庫也不能少,我們可以簡單的實現幾個。

map

map 比較簡單,就是將一個 返回值為包裝值的函數 作用于一個包裝(Wrapped)值的過程, 這里的包裝值可以理解為可以包含其他值的一種結構,例如 Swift 中的數組,可選類型都是包裝值。它們都有重載的 map, flatMap等函數。以數組為例,我們經常這樣使用:

let images = ["1", "2", "3"].map{ UIImage(named: $0) }

現在來實現我們的 map 函數:

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
}

我同時給 Result 也實現了 map 函數:

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 的不同主要體現在 transform 函數的返回值不同。map 接受的函數返回值類型是 U類型,而 flatMap 接受的函數返回值類型是 U?類型。例如對于一個可選值,可以這樣調用:

let aString: String? = "¥99.9"
let price = aString.flatMap{ Float($0)}
// Price is nil

我們這里 flatMap 和 Swift 中數組以及可選型中的 flatMap 保持了一致。

所以我們的 flatMap 應該是這樣定義:flatMap (_ transform: @escaping (Value) -> Signal ) -> Signal 。

理解了 flatMap 和 map 的不同,實現起來也就很簡單了:

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
}

現在我們可以模擬一個網絡請求來測試 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 ,我們可以很簡單的將一個 Signal 轉換為另一個 Signal , 這在我們處理多個請求嵌套時就會很方便了。

寫在最后

上面通過100 多行的代碼就實現了一個簡單的響應式編程庫。不過對于一個庫來說,以上的內容還遠遠不夠。現在的 Signal 還不具有原子性,要作為一個實際可用的庫,應該是線程安的。還有我們對 Disposable 的處理也不夠優雅,可以模仿 RxSwift 中 DisposeBag 的做法。上面這些問題可以留給讀者自己去思考了。

 

 

來自:http://www.cocoachina.com/ios/20171208/21481.html

 

 本文由用戶 MartinHenne 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!