前言
函數式響應式編程框架我們應該也用得比較多了,如ReactiveCocoa
、ReactiveX
系列(RxSwift、RxKotlin、RxJava)
,這些框架內部實現都是基于函數式編程的思想來構建的。還記得前不久面試的時候面試官有問道:“有閱讀過ReactiveCocoa的源碼嗎?有沒有看過其中的核心函數bind?你知道這個函數如何實現的嗎?
”。在回答這個問題時,如果面試者只是單純的看過RAC源碼,雖能憑自己的印象說出這個方法的大概流程,不過對其中的思想可能也只是一知半解,但如果你充分了解過函數式編程,熟悉Monad
概念,就能知道bind
方法其實就是Monad
概念中的一部分,RAC正是利用Monad
來實現它的Signal
。此時你就可以向面試官開始你的表演了。
如標題所述,在這篇文章中我們將利用函數式編程的思想,去構建一個小型的響應式框架。它具有響應回調的能力,且能將一個個事件數據抽象成管道中流動的流體,我們可以對這些事件數據進行若干的轉換,最后再訂閱它們。
本文為《函數式編程》系列文章中的第三篇,若大家對函數式編程感興趣,可以閱讀系列的前兩篇文章:
原理
函數式響應式的本質是什么
先附上一張流轉換思想的概念圖:
在日常項目邏輯的構建中,我們總會對一些數據進行轉換運算,這里我們將數據的轉換過程抽象成一條包裹著流動數據的管道,數據以流的形式在這條管道中流通,當經過轉換器時,原始的數據流將會被轉換成新的數據流,然后繼續流動下去。針對數據的轉換運算,我們會使用一些函數/方法,將運算的數據作為實參傳入函數中/對運算的對象調用方法,得到轉換后的結果。此時整個運算將會同步
運行,轉換函數接收舊數據進行轉換,成功后返回新的數據。除此之外,你還可以在這個管道中安置多個轉換器,數據在通過若干的轉換器后便轉換成了最終我們所期望的結果值,并從管道中流出。
不過,事實上項目邏輯中也會涉及到許多非同步
進行的操作,如某些較為耗時的操作(數據庫操作、網絡請求)、基于事件循環(RunLoop)的事件監聽處理(屏幕觸摸監聽、設備傳感器監聽),這些操作有的會在后臺創建新的線程進行處理,當處理完成后將數據饋回到主線程中,有的則是會在整個運行循環中通過對每一次循環周期從事件隊列中取得需要處理的事件,派發到相應的Handler中。對于這些操作,它們都具有共同點,那就是:數據返回的過程都是通過回調(Callback)
來實現的。
對于如何將流轉換
的思想用于Callback
上,就是函數式響應式所探討解決的問題。
在前不久我有幸參與了中國2017年Swift大會,會議邀請了RxSwift的作者前來演講,在演講中他闡明了RxSwift的本質:
RxSwift just a callback! (RxSwift就是一個回調)
可能這里有人會有疑問:為什么回調不使用一個簡單的代理模式或者一個閉包,反而構建起這么復雜且重量級的框架?因為,這些函數式響應式框架要做的事情就是讓回調結合流轉換的思想,讓開發者只專注于數據的轉換過程而不必多花精力在回調的設計上,輕松寫出簡潔優雅的回調過程。
核心思想
流轉換的思想為將數據事件抽象成管道中流通的流體,用過轉換器轉換成新的數據事件
,若加上回調
的實現,我們可以說這條管道是建立在回調上的。這時候,我們就可以理清管道和數據的關系:建立在回調上的管道包裹著數據。換句話說,具有回調能力的管道作為一個Context(上下文)
,包裹著基本的數據值,并且它還擁有某種運算的能力,那就是觸發事件、監聽回調
,而這種運算不需要我們去花精力放在上面,我們只想專注于數據的轉換。
看到上面對函數式響應式的描述,你或許也發現了這跟函數式編程里面一個十分重要的概念高度匹配,那就是Monad(單子)
。是的,函數式響應式的核心其實就是建立在Monad
之上,所以,要實現函數式響應式,我們須構建出一個Monad
,可以把它叫做響應式Monad。
看過ReactiveCocoa
源碼的小伙伴可能知道,RACSignal
中具有方法bind
和派生類RACReturnSignal
,它們就是用來實現Monad
中的bind
和return
函數,所以,Signal
就是一個Monad
。不過我們這里需要知道的是,ReactiveCocoa
中的bind
方法并非完全標準的Monad bind
函數,它在參數類型上有所變化,在外表封裝多了一層RACSignalBindBlock
,要說最接近Monad bind
的,應該就屬RACSignal
中的flattenMap
方法了(RACSignal的flattenMap方法也是基于bind包裝)。所以,實現了響應式Monad,你就能免費得到flattenMap
方法。
因為Monad
必定也是一個Functor
,所以當你實現一個響應式Monad后,相應的Functor
中的map
方法你就能很輕易地實現出來了。是的,map
方法并非RACSignal
所特有的,其也是來自于函數式編程中的Functor
。
實現
因為個人熱衷于Swift,接下來我將基于Swift語言實現一個簡單的函數式響應式框架。
Event
首先我們來實現Event(事件)
,像ReactiveCocoa
、RxSwift
中,事件具有三種類型,分別是:
- next 表示一個數據流元素
- completed 表示數據流已經完成
- error 表示數據流中產生了錯誤
這個我實現的事件就簡單一點,它僅具有next
和error
類型:
enum Event<E> {
case next(E)
case error(Error)
}
Event
中的泛型E
代表其中數據元素的類型。這里需要注意的是,當事件類型為error
時,其關聯的錯誤實例并沒有類型限制,這里為了簡單演示我沒有添加約束錯誤實例的泛型,大家在后面如果嘗試自己去實現的話可以稍作優化,如:
enum Event<E, R> where R: Error {
case next(E)
case error(R)
}
Observer
Observer
要做的事情有兩個,分別是發送事件
以及監聽事件
。
// MARK: - Protocol - Observer
protocol ObserverType {
associatedtype E
var action: (Event<E>) -> () { get }
init(_ action: @escaping (Event<E>) -> ())
func send(_ event: Event<E>)
}
extension ObserverType {
func send(_ event: Event<E>) {
action(event)
}
func sendNext(_ value: E) {
send(.next(value))
}
func sendError(_ error: Error) {
send(.error(error))
}
}
// MARK: - Class - Observer
final class Observer<Element>: ObserverType {
typealias E = Element
let action: (Event<E>) -> ()
init(_ action: @escaping (Event<E>) -> ()) {
self.action = action
}
}
通過send
方法,Observer
可以發送出事件,而通過實現一個閉包并將其傳入到Observer
的構造器中,我們就可以監聽到Observer
發出的事件。
Signal
接下來就是重頭戲:Signal
(命名是我從ReactiveCocoa
中直接借鑒而來),它就是我們上面所提到的響應式Monad
,整個函數式響應式的核心。
我們先來看看SignalType
協議:
// MARK: - Protocol - Signal
protocol SignalType {
associatedtype E
func subscribe(_ observer: Observer<E>)
}
extension SignalType {
func subscribe(next: ((E) -> ())? = nil,
error: ((Error) -> ())? = nil) {
let observer = Observer<E> { event in
switch event {
case .error(let e):
error?(e)
case .next(let element):
next?(element)
}
}
subscribe(observer)
}
}
協議聲明了用于訂閱事件的方法subscribe(_:)
,這個方法接收了一個Observer
作為參數,基于此方法我們就可以擴展出專門針對特殊事件類型(next、error)的訂閱方法:subscribe(next:error:)
。
接下來就是Signal
的實現:
// MARK: - Class - Signal
final class Signal<Element>: SignalType {
typealias E = Element
private var value: E?
private var observer: Observer<E>?
init(value: E) {
self.value = value
}
init(_ creater: (Observer<E>) -> ()) {
let observer = Observer(action)
creater(observer)
}
func action(_ event: Event<E>) {
observer?.action(event)
}
static func `return`(_ value: E) -> Signal<E> {
return Signal(value: value)
}
func subscribe(_ observer: Observer<E>) {
if let value = value { observer.sendNext(value) }
self.observer = observer
}
static func pipe() -> (Observer<E>, Signal<E>) {
var observer: Observer<E>!
let signal = Signal<E> {
observer = $0
}
return (observer, signal)
}
}
我們可以看到Signal
內部具有一個成員屬性observer
,當我們調用subscribe(_:)
方法時就將傳入的參數賦予給這個成員。對于另一個成員屬性value
,它的作用是為了讓Signal
實現Monad return
函數,我在《函數式編程》系列文章的前面已經介紹過,Monad return
函數就是將一個基本的數據包裹在一個Monad
上下文中。所以在Signal
中我定義了類方法return(_:)
,內部調用了針對于value
初始化的Signal
構造器init(value: E)
,將一個基本的數據賦予給了value
成員屬性。在subscribe(_:)
方法的實現中,我們首先對value
做非空判斷,若此時value
存在,傳入的observer
參數將發送關聯了value
的next
事件,這樣做是為了保證整個Signal
符合Monad
特性。
接著到init(_ creater: (Observer<E>) -> ())
構造方法,這個方法接受一個閉包,閉包里面做的,就是進行某些運算處理邏輯或事件監聽,如網絡請求、事件監聽等。閉包帶有一個Observer
類型的參數,當閉包中的運算處理邏輯完成或者接收到事件回調時,就利用這個Observer
發送事件。在這個構造方法實現的內部,我首先將Signal
自己的action(_:)
方法作為參數傳入Observer
的構造器從而創建了一個Observer
實例,其中,action(_:)
方法做的事情是:指使成員屬性observer
將自己接收到的事件參數轉發出去。這里的設計比較巧妙,我們在構造器閉包類型參數creater
中進行處理邏輯或事件監聽,若得到結果,將使用閉包中的Observer
參數發送事件,事件將會傳遞到訂閱了這個Signal
的訂閱者中,從而觸發相關回調。
這里可能有人會有疑惑:為什么需要用兩個observer
來傳遞事件?可以在subscribe(_:)
方法調用的時候再順便調用creater
閉包,把接收到的訂閱者傳入即可。其實,我這么做的目的是為了保證creater
的調用跟init(_ creater: (Observer<E>) -> ())
同步進行,因為在Signal
中我提供了pipe
方法。
pipe
方法返回一個二元組,第一項為Observer
,我們可以利用它來發送事件,第二項為Signal
,我們可以通過它來訂閱事件,它就像RxSwift
中的Subject
,只不過這里我將事件發送者與訂閱者區分開了。這里有一個需要注意的地方:
上面說到,對于我們使用
pipe
函數獲取到的Observer
,其內部的action
成員屬性來自于Signal
的action(_:)
方法,這個方法引用到了Signal
中的成員屬性。由此,我們可以推出此時Observer
對Signal
具有引用的關系,Observer
不釋放,Signal
也會一直保留。
接下來就是讓Signal
實現Monad
的bind
方法了:
// MARK: - Monad - Signal
extension Signal {
func bind<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> {
return Signal<O> { [weak self] observer in
self?.subscribe(next: { element in
f(element).subscribe(observer)
}, error: { error in
observer.sendError(error)
})
}
}
func flatMap<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> {
return bind(f)
}
func map<O>(_ f: @escaping (E) -> O) -> Signal<O> {
return bind { element in
return Signal<O>.return(f(element))
}
}
}
bind
方法接受一個函數作為參數,這個函數的類型為(E) -> Signal<O>
,E
泛型為舊Signal
元素中的類型,O
則是新Signal
元素中的類型,這個bind
方法其實跟ReactiveCocoa
的flattenMap
或是RxSwift
中的flatMap
做的事情一樣,所以在下面的flatMap
方法的實現中我只是直接地調用bind
方法。很多人俗稱這個過程為降維。
在bind
方法的實現中,我們返回一個新的Signal
,為了構造這個Signal
,我們使用初始化方法init(_ creater: (Observer<E>) -> ())
,在creater
閉包中訂閱舊的Signal
。倘若舊Signal
的Observer
發出error
事件,則直接把error
事件中關聯的Error
實例提取出來,通過creater
閉包中作為參數傳入的Observer
包裹起來再傳遞出去;而若是舊Signal
的Observer
發出next
事件,則先把next
關聯的數據元素提取出來,通過調用bind
傳進來的函數,獲取一個中間層的Signal
,再通過對這個中間層Signal
進行訂閱,將事件傳遞到新的Signal
中。
creater
閉包中我使用了[weak self]
捕獲列表來對舊Signal
進行若引用以防止循環引用的發生,為什么這里可能會發生循環引用?上面提到過,Observer
會引用Signal
,而在creater
閉包中舊的Signal
將引用新Signal
的Observer
,從而可以推出舊的Signal
會對新Signal
持引用關系,這里如果不留意的話會造成循環引用。
Monad
中的bind
方法將自動處理上下文。在Signal
中,bind
則幫我們自己處理好事件的訂閱、轉移、傳遞,而我們只需要專注于純數據的轉換。
map
方法的實現十分簡單,通過在內部調用bind
方法,并將最終數據通過return
包裹進Signal
上下文中,在這里我就不多說了。
以上,我們的響應式Monad
就實現完成了!
以上只是非常簡單地實現函數式響應式,目的是為了簡單介紹如何利用函數式編程思想去完成響應式的操作,其中并沒有考慮有關跨線程調度的問題,大家如果有興趣的可以自己嘗試去進行相關優化。
下面我們來測試使用一下。
簡單使用
通過creater閉包構建Signal
let mSignal: Signal<Int> = Signal { observer in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
observer.sendNext(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
observer.sendNext(2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
observer.sendNext(3)
}
}
mSignal.map { $0 + 1 }.map { $0 * 3 }.map { "The number is \($0)" }.subscribe(next: { numString in
print(numString)
})
輸出:
The number is 6
The number is 9
The number is 12
通過pipe構建Signal
let (mObserver, mSignal) = Signal<Int>.pipe()
mSignal.map { $0 * 3 }.map { $0 + 1 }.map { "The value is \($0)" }.subscribe(next: { value in
print(value)
})
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
mObserver.sendNext(3)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
mObserver.sendNext(2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
mObserver.sendNext(1)
}
輸出:
The value is 10
The value is 7
The value is 4
擴展
接下來我們對剛剛實現的函數式響應式進行擴展,關聯一些平時我們常用到的類。
UIControl
對UIControl
的觸發事件進行監聽,傳統的做法是通過調用addTarget(_:, action:, for:)
方法,傳入target以及一個回調函數Selector。很多人比較厭倦這種方法,覺得每次監聽事件都需要定義一個事件處理函數,比較麻煩,希望能直接通過閉包回調事件觸發。
這里只需簡單地封裝一下即可滿足這種需求:
final class ControlTarget: NSObject {
private let _callback: (UIControl) -> ()
init(control: UIControl, events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
_callback = callback
super.init()
control.addTarget(self, action: #selector(ControlTarget._handle(control:)), for: events)
}
@objc private func _handle(control: UIControl) {
_callback(control)
}
}
fileprivate var targetsKey: UInt8 = 23
extension UIControl {
func on(events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
var targets = objc_getAssociatedObject(self, &targetsKey) as? [UInt: ControlTarget] ?? [:]
targets[events.rawValue] = ControlTarget(control: self, events: events, callback: callback)
objc_setAssociatedObject(self, &targetsKey, targets, .OBJC_ASSOCIATION_RETAIN)
}
}
在這里我間接利用ControlTarget
對象來將UIControl
事件觸發傳遞到閉包中,并通過關聯對象
來使得UIControl
保持對ControlTarget
的引用,以防止其被自動釋放。經過上面簡單的封裝后,我們就能很方面地利用閉包監聽UIControl
的事件回調:
button.on(events: .touchUpInside) { button in
print("\(button) - TouchUpInside")
}
button.on(events: .touchUpOutside) { button in
print("\(button) - TouchUpOutside")
}
由此,我們可以簡單地基于上面的封裝來擴展我們的函數式響應式:
extension UIControl {
func trigger(events: UIControlEvents) -> Signal<UIControl> {
return Signal { [weak self] observer in
self?.on(events: events, callback: { control in
observer.sendNext(control)
})
}
}
var tap: Signal<()> {
return trigger(events: .touchUpInside).map { _ in () }
}
}
trigger(events:)
方法傳入一個需要進行監聽的事件類型,返回一個Signal
,當對應的事件觸發時,Signal
中則會發射出事件。而tap
返回的則是針對TouchUpInside
事件觸發的Signal
。
使用起來跟RxSwift
或ReactiveCocoa
一樣,十分簡潔優雅:
button.tap.map { _ in "Tap~" }.subscribe(next: { message in
print(message)
})
上面整個過程的引用關系為: UIControl -> ControlTarget -> _callback -> Observer -> Signal,由此我們知道,只要保持對
UIControl
的引用,那么其所關聯的事件監聽Signal
則不會被自動釋放,可以在整個RunLoop
中持續工作,
NotificationCenter
將函數式響應式適配控制中心,方法跟上面對UIControl
的擴展一樣,通過一個中間層NotificationObserver
來做事件的傳遞轉發:
final class NotificationObserver: NSObject {
private unowned let _center: NotificationCenter
private let _callback: (Notification) -> ()
init(center: NotificationCenter, name: Notification.Name, object: Any?, callback: @escaping (Notification) -> ()) {
_center = center
_callback = callback
super.init()
center.addObserver(self, selector: #selector(NotificationObserver._handle(notification:)), name: name, object: object)
}
@objc private func _handle(notification: Notification) {
_callback(notification)
}
deinit {
_center.removeObserver(self)
}
}
fileprivate var observersKey: UInt = 78
extension NotificationCenter {
func callback(_ name: Notification.Name, object: Any?, callback: @escaping (Notification) -> ()) {
var observers = objc_getAssociatedObject(self, &observersKey) as? [String: NotificationObserver] ?? [:]
observers[name.rawValue] = NotificationObserver(center: self, name: name, object: object, callback: callback)
objc_setAssociatedObject(self, &observersKey, observers, .OBJC_ASSOCIATION_RETAIN)
}
func listen(_ name: Notification.Name, object: Any?) -> Signal<Notification> {
// Warning: 注意object可能對返回的Signal進行引用,從而造成循環引用
return Signal { [weak self] observer in
self?.callback(name, object: object, callback: { notification in
observer.sendNext(notification)
})
}
}
}
由此,我們可以基于上面對NotificationCenter
的響應式擴展,來完成對UITextFiled
文字變化的監聽:
extension UITextField {
var listen: Signal<String?> {
return NotificationCenter.default.listen(.UITextFieldTextDidChange, object: self).map { $0.object as? UITextField }.map { $0?.text }
}
}
// 使用
textField.listen.map { "Input: \($0 ?? "")" }.subscribe(next: {
print($0)
})
方法調用監聽 / 代理調用監聽
我們有時候想監聽某個對象中指定方法的調用,來實現面向切面編程或者埋點,另外,當函數式響應式被引入后,我們希望它能充當代理的職責,監聽代理方法的調用。為此我們可以通過對函數式響應式進行擴展來支持上面的需求。不過要做這件事情并不簡單,這里面要涉及多種Runtime
特性,如方法交換、方法動態派發、isa交換等Runtime黑科技,要實踐它可能需要投入較大精力,花費較長時間。因本人能力與時間有限,沒有去編寫相應的代碼,若大家有興趣可以嘗試一下,而后期如果我做了相關的努力,也會公布出來。
為什么沒有Disposable
若我們接觸過RxSwift
、ReactiveSwift
,我們會發現每次我們訂閱完一個Observable
或者Signal
后,會得到訂閱方法返回的一個專門用于回收資源的實例,比如RxSwift
中的Disposable
,我們可以通過在某個時機調用它的dispose
方法,或者將其放入一個DisposeBag
中來使得資源在最后得到充分的回收。
再來看回我們在上面實現的響應式框架,因為這個框架的實現非常簡單,并不會在訂閱后返回一個專門提供給我們釋放資源的實例,所以我們在使用它的時候要密切留意資源的存活與釋放問題。這里舉一個例子:
在上面,我們對函數式響應式進行針對UIControl
的適配時,是通過一個中間層ControlTarget
來完成的,為了保持這個ControlTarget
實例的存活,使得它不會被自動釋放,我們先用一個集合來包裹住它,并將這個集合設置為目標UIControl
的關聯對象。此時我們可以將這個中間層ControlTarget
看做是這個事件流管道中的一個資源,這個資源的銷毀是由目標UIControl
來決定的。
對于RxSwift
來說,它實現對UIControl
的擴展原理跟我們寫的差不多,也是通過一個中間層來完成,但是對于中間層資源的保活與銷毀,它采用的是另一種方法,我們可以看下這段RxSwift
的源碼(為了簡單,刪掉了一些無關的代碼):
class RxTarget {
private var retainSelf: RxTarget?
init() {
self.retainSelf = self
}
func dispose() {
self.retainSelf = nil
}
}
這個類型的保活方式十分巧妙,它利用自己對自己的循環引用來使得維持生存,而當調用dispose
方法時,它將解開對自己的循環引用,從而將自己銷毀。
通過上面兩個例子的對比,我們可以知道,對于我們自己實現的響應式框架,我們需要把某些精力放在對資源的保活與釋放上,而像RxSwift
,它則提供一個統一的資源管理方式,相比起來更加清晰優雅,大家有興趣可以實現一下這種方式。
相關鏈接
Github - ReactiveObjc
Github - ReactiveCocoa
Github - RxSwift
本文純屬個人見解,若大家發現文章部分有誤,歡迎在評論區提出。