前言
函數(shù)式響應(yīng)式編程框架我們應(yīng)該也用得比較多了阀捅,如ReactiveCocoa
拂铡、ReactiveX
系列(RxSwift、RxKotlin终惑、RxJava)
绍在,這些框架內(nèi)部實(shí)現(xiàn)都是基于函數(shù)式編程的思想來(lái)構(gòu)建的。還記得前不久面試的時(shí)候面試官有問(wèn)道:“有閱讀過(guò)ReactiveCocoa的源碼嗎雹有?有沒(méi)有看過(guò)其中的核心函數(shù)bind偿渡?你知道這個(gè)函數(shù)如何實(shí)現(xiàn)的嗎?
”霸奕。在回答這個(gè)問(wèn)題時(shí)溜宽,如果面試者只是單純的看過(guò)RAC源碼,雖能憑自己的印象說(shuō)出這個(gè)方法的大概流程质帅,不過(guò)對(duì)其中的思想可能也只是一知半解适揉,但如果你充分了解過(guò)函數(shù)式編程留攒,熟悉Monad
概念,就能知道bind
方法其實(shí)就是Monad
概念中的一部分嫉嘀,RAC正是利用Monad
來(lái)實(shí)現(xiàn)它的Signal
炼邀。此時(shí)你就可以向面試官開(kāi)始你的表演了。
如標(biāo)題所述剪侮,在這篇文章中我們將利用函數(shù)式編程的思想拭宁,去構(gòu)建一個(gè)小型的響應(yīng)式框架。它具有響應(yīng)回調(diào)的能力瓣俯,且能將一個(gè)個(gè)事件數(shù)據(jù)抽象成管道中流動(dòng)的流體杰标,我們可以對(duì)這些事件數(shù)據(jù)進(jìn)行若干的轉(zhuǎn)換,最后再訂閱它們降铸。
本文為《函數(shù)式編程》系列文章中的第三篇在旱,若大家對(duì)函數(shù)式編程感興趣,可以閱讀系列的前兩篇文章:
原理
函數(shù)式響應(yīng)式的本質(zhì)是什么
先附上一張流轉(zhuǎn)換思想的概念圖:
在日常項(xiàng)目邏輯的構(gòu)建中,我們總會(huì)對(duì)一些數(shù)據(jù)進(jìn)行轉(zhuǎn)換運(yùn)算谅畅,這里我們將數(shù)據(jù)的轉(zhuǎn)換過(guò)程抽象成一條包裹著流動(dòng)數(shù)據(jù)的管道登渣,數(shù)據(jù)以流的形式在這條管道中流通,當(dāng)經(jīng)過(guò)轉(zhuǎn)換器時(shí)毡泻,原始的數(shù)據(jù)流將會(huì)被轉(zhuǎn)換成新的數(shù)據(jù)流胜茧,然后繼續(xù)流動(dòng)下去。針對(duì)數(shù)據(jù)的轉(zhuǎn)換運(yùn)算仇味,我們會(huì)使用一些函數(shù)/方法呻顽,將運(yùn)算的數(shù)據(jù)作為實(shí)參傳入函數(shù)中/對(duì)運(yùn)算的對(duì)象調(diào)用方法,得到轉(zhuǎn)換后的結(jié)果丹墨。此時(shí)整個(gè)運(yùn)算將會(huì)同步
運(yùn)行廊遍,轉(zhuǎn)換函數(shù)接收舊數(shù)據(jù)進(jìn)行轉(zhuǎn)換,成功后返回新的數(shù)據(jù)贩挣。除此之外喉前,你還可以在這個(gè)管道中安置多個(gè)轉(zhuǎn)換器,數(shù)據(jù)在通過(guò)若干的轉(zhuǎn)換器后便轉(zhuǎn)換成了最終我們所期望的結(jié)果值王财,并從管道中流出卵迂。
不過(guò),事實(shí)上項(xiàng)目邏輯中也會(huì)涉及到許多非同步
進(jìn)行的操作绒净,如某些較為耗時(shí)的操作(數(shù)據(jù)庫(kù)操作见咒、網(wǎng)絡(luò)請(qǐng)求)、基于事件循環(huán)(RunLoop)的事件監(jiān)聽(tīng)處理(屏幕觸摸監(jiān)聽(tīng)挂疆、設(shè)備傳感器監(jiān)聽(tīng))改览,這些操作有的會(huì)在后臺(tái)創(chuàng)建新的線程進(jìn)行處理哎垦,當(dāng)處理完成后將數(shù)據(jù)饋回到主線程中,有的則是會(huì)在整個(gè)運(yùn)行循環(huán)中通過(guò)對(duì)每一次循環(huán)周期從事件隊(duì)列中取得需要處理的事件恃疯,派發(fā)到相應(yīng)的Handler中漏设。對(duì)于這些操作,它們都具有共同點(diǎn)今妄,那就是:數(shù)據(jù)返回的過(guò)程都是通過(guò)回調(diào)(Callback)
來(lái)實(shí)現(xiàn)的郑口。
對(duì)于如何將流轉(zhuǎn)換
的思想用于Callback
上,就是函數(shù)式響應(yīng)式所探討解決的問(wèn)題盾鳞。
在前不久我有幸參與了中國(guó)2017年Swift大會(huì)犬性,會(huì)議邀請(qǐng)了RxSwift的作者前來(lái)演講,在演講中他闡明了RxSwift的本質(zhì):
RxSwift just a callback! (RxSwift就是一個(gè)回調(diào))
可能這里有人會(huì)有疑問(wèn):為什么回調(diào)不使用一個(gè)簡(jiǎn)單的代理模式或者一個(gè)閉包腾仅,反而構(gòu)建起這么復(fù)雜且重量級(jí)的框架乒裆?因?yàn)椋@些函數(shù)式響應(yīng)式框架要做的事情就是讓回調(diào)結(jié)合流轉(zhuǎn)換的思想推励,讓開(kāi)發(fā)者只專(zhuān)注于數(shù)據(jù)的轉(zhuǎn)換過(guò)程而不必多花精力在回調(diào)的設(shè)計(jì)上鹤耍,輕松寫(xiě)出簡(jiǎn)潔優(yōu)雅的回調(diào)過(guò)程。
核心思想
流轉(zhuǎn)換的思想為將數(shù)據(jù)事件抽象成管道中流通的流體验辞,用過(guò)轉(zhuǎn)換器轉(zhuǎn)換成新的數(shù)據(jù)事件
稿黄,若加上回調(diào)
的實(shí)現(xiàn),我們可以說(shuō)這條管道是建立在回調(diào)上的跌造。這時(shí)候杆怕,我們就可以理清管道和數(shù)據(jù)的關(guān)系:建立在回調(diào)上的管道包裹著數(shù)據(jù)。換句話說(shuō)壳贪,具有回調(diào)能力的管道作為一個(gè)Context(上下文)
陵珍,包裹著基本的數(shù)據(jù)值,并且它還擁有某種運(yùn)算的能力违施,那就是觸發(fā)事件互纯、監(jiān)聽(tīng)回調(diào)
,而這種運(yùn)算不需要我們?nèi)セňΨ旁谏厦孀硗兀覀冎幌雽?zhuān)注于數(shù)據(jù)的轉(zhuǎn)換伟姐。
看到上面對(duì)函數(shù)式響應(yīng)式的描述收苏,你或許也發(fā)現(xiàn)了這跟函數(shù)式編程里面一個(gè)十分重要的概念高度匹配亿卤,那就是Monad(單子)
。是的鹿霸,函數(shù)式響應(yīng)式的核心其實(shí)就是建立在Monad
之上排吴,所以,要實(shí)現(xiàn)函數(shù)式響應(yīng)式懦鼠,我們須構(gòu)建出一個(gè)Monad
钻哩,可以把它叫做響應(yīng)式Monad屹堰。
看過(guò)ReactiveCocoa
源碼的小伙伴可能知道,RACSignal
中具有方法bind
和派生類(lèi)RACReturnSignal
街氢,它們就是用來(lái)實(shí)現(xiàn)Monad
中的bind
和return
函數(shù)扯键,所以,Signal
就是一個(gè)Monad
珊肃。不過(guò)我們這里需要知道的是荣刑,ReactiveCocoa
中的bind
方法并非完全標(biāo)準(zhǔn)的Monad bind
函數(shù),它在參數(shù)類(lèi)型上有所變化伦乔,在外表封裝多了一層RACSignalBindBlock
厉亏,要說(shuō)最接近Monad bind
的,應(yīng)該就屬RACSignal
中的flattenMap
方法了(RACSignal的flattenMap方法也是基于bind包裝)烈和。所以爱只,實(shí)現(xiàn)了響應(yīng)式Monad,你就能免費(fèi)得到flattenMap
方法招刹。
因?yàn)?code>Monad必定也是一個(gè)Functor
恬试,所以當(dāng)你實(shí)現(xiàn)一個(gè)響應(yīng)式Monad后,相應(yīng)的Functor
中的map
方法你就能很輕易地實(shí)現(xiàn)出來(lái)了疯暑。是的忘渔,map
方法并非RACSignal
所特有的,其也是來(lái)自于函數(shù)式編程中的Functor
缰儿。
實(shí)現(xiàn)
因?yàn)閭€(gè)人熱衷于Swift畦粮,接下來(lái)我將基于Swift語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單的函數(shù)式響應(yīng)式框架。
Event
首先我們來(lái)實(shí)現(xiàn)Event(事件)
乖阵,像ReactiveCocoa
宣赔、RxSwift
中,事件具有三種類(lèi)型瞪浸,分別是:
- next 表示一個(gè)數(shù)據(jù)流元素
- completed 表示數(shù)據(jù)流已經(jīng)完成
- error 表示數(shù)據(jù)流中產(chǎn)生了錯(cuò)誤
這個(gè)我實(shí)現(xiàn)的事件就簡(jiǎn)單一點(diǎn)儒将,它僅具有next
和error
類(lèi)型:
enum Event<E> {
case next(E)
case error(Error)
}
Event
中的泛型E
代表其中數(shù)據(jù)元素的類(lèi)型。這里需要注意的是对蒲,當(dāng)事件類(lèi)型為error
時(shí)钩蚊,其關(guān)聯(lián)的錯(cuò)誤實(shí)例并沒(méi)有類(lèi)型限制,這里為了簡(jiǎn)單演示我沒(méi)有添加約束錯(cuò)誤實(shí)例的泛型蹈矮,大家在后面如果嘗試自己去實(shí)現(xiàn)的話可以稍作優(yōu)化砰逻,如:
enum Event<E, R> where R: Error {
case next(E)
case error(R)
}
Observer
Observer
要做的事情有兩個(gè),分別是發(fā)送事件
以及監(jiān)聽(tīng)事件
泛鸟。
// 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
}
}
通過(guò)send
方法蝠咆,Observer
可以發(fā)送出事件,而通過(guò)實(shí)現(xiàn)一個(gè)閉包并將其傳入到Observer
的構(gòu)造器中,我們就可以監(jiān)聽(tīng)到Observer
發(fā)出的事件刚操。
Signal
接下來(lái)就是重頭戲:Signal
(命名是我從ReactiveCocoa
中直接借鑒而來(lái))闸翅,它就是我們上面所提到的響應(yīng)式Monad
,整個(gè)函數(shù)式響應(yīng)式的核心菊霜。
我們先來(lái)看看SignalType
協(xié)議:
// 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)
}
}
協(xié)議聲明了用于訂閱事件的方法subscribe(_:)
坚冀,這個(gè)方法接收了一個(gè)Observer
作為參數(shù),基于此方法我們就可以擴(kuò)展出專(zhuān)門(mén)針對(duì)特殊事件類(lèi)型(next鉴逞、error)的訂閱方法:subscribe(next:error:)
遗菠。
接下來(lái)就是Signal
的實(shí)現(xiàn):
// 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
內(nèi)部具有一個(gè)成員屬性observer
,當(dāng)我們調(diào)用subscribe(_:)
方法時(shí)就將傳入的參數(shù)賦予給這個(gè)成員华蜒。對(duì)于另一個(gè)成員屬性value
辙纬,它的作用是為了讓Signal
實(shí)現(xiàn)Monad return
函數(shù),我在《函數(shù)式編程》系列文章的前面已經(jīng)介紹過(guò)叭喜,Monad return
函數(shù)就是將一個(gè)基本的數(shù)據(jù)包裹在一個(gè)Monad
上下文中贺拣。所以在Signal
中我定義了類(lèi)方法return(_:)
,內(nèi)部調(diào)用了針對(duì)于value
初始化的Signal
構(gòu)造器init(value: E)
捂蕴,將一個(gè)基本的數(shù)據(jù)賦予給了value
成員屬性譬涡。在subscribe(_:)
方法的實(shí)現(xiàn)中,我們首先對(duì)value
做非空判斷啥辨,若此時(shí)value
存在涡匀,傳入的observer
參數(shù)將發(fā)送關(guān)聯(lián)了value
的next
事件,這樣做是為了保證整個(gè)Signal
符合Monad
特性溉知。
接著到init(_ creater: (Observer<E>) -> ())
構(gòu)造方法陨瘩,這個(gè)方法接受一個(gè)閉包,閉包里面做的级乍,就是進(jìn)行某些運(yùn)算處理邏輯或事件監(jiān)聽(tīng)舌劳,如網(wǎng)絡(luò)請(qǐng)求、事件監(jiān)聽(tīng)等玫荣。閉包帶有一個(gè)Observer
類(lèi)型的參數(shù)甚淡,當(dāng)閉包中的運(yùn)算處理邏輯完成或者接收到事件回調(diào)時(shí),就利用這個(gè)Observer
發(fā)送事件捅厂。在這個(gè)構(gòu)造方法實(shí)現(xiàn)的內(nèi)部贯卦,我首先將Signal
自己的action(_:)
方法作為參數(shù)傳入Observer
的構(gòu)造器從而創(chuàng)建了一個(gè)Observer
實(shí)例,其中焙贷,action(_:)
方法做的事情是:指使成員屬性observer
將自己接收到的事件參數(shù)轉(zhuǎn)發(fā)出去撵割。這里的設(shè)計(jì)比較巧妙,我們?cè)跇?gòu)造器閉包類(lèi)型參數(shù)creater
中進(jìn)行處理邏輯或事件監(jiān)聽(tīng)盈厘,若得到結(jié)果睁枕,將使用閉包中的Observer
參數(shù)發(fā)送事件官边,事件將會(huì)傳遞到訂閱了這個(gè)Signal
的訂閱者中沸手,從而觸發(fā)相關(guān)回調(diào)外遇。
這里可能有人會(huì)有疑惑:為什么需要用兩個(gè)observer
來(lái)傳遞事件?可以在subscribe(_:)
方法調(diào)用的時(shí)候再順便調(diào)用creater
閉包契吉,把接收到的訂閱者傳入即可跳仿。其實(shí),我這么做的目的是為了保證creater
的調(diào)用跟init(_ creater: (Observer<E>) -> ())
同步進(jìn)行捐晶,因?yàn)樵?code>Signal中我提供了pipe
方法菲语。
pipe
方法返回一個(gè)二元組,第一項(xiàng)為Observer
惑灵,我們可以利用它來(lái)發(fā)送事件山上,第二項(xiàng)為Signal
,我們可以通過(guò)它來(lái)訂閱事件英支,它就像RxSwift
中的Subject
佩憾,只不過(guò)這里我將事件發(fā)送者與訂閱者區(qū)分開(kāi)了。這里有一個(gè)需要注意的地方:
上面說(shuō)到干花,對(duì)于我們使用
pipe
函數(shù)獲取到的Observer
妄帘,其內(nèi)部的action
成員屬性來(lái)自于Signal
的action(_:)
方法,這個(gè)方法引用到了Signal
中的成員屬性池凄。由此抡驼,我們可以推出此時(shí)Observer
對(duì)Signal
具有引用的關(guān)系,Observer
不釋放肿仑,Signal
也會(huì)一直保留致盟。
接下來(lái)就是讓Signal
實(shí)現(xiàn)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
方法接受一個(gè)函數(shù)作為參數(shù),這個(gè)函數(shù)的類(lèi)型為(E) -> Signal<O>
尤慰,E
泛型為舊Signal
元素中的類(lèi)型勾邦,O
則是新Signal
元素中的類(lèi)型,這個(gè)bind
方法其實(shí)跟ReactiveCocoa
的flattenMap
或是RxSwift
中的flatMap
做的事情一樣割择,所以在下面的flatMap
方法的實(shí)現(xiàn)中我只是直接地調(diào)用bind
方法眷篇。很多人俗稱(chēng)這個(gè)過(guò)程為降維。
在bind
方法的實(shí)現(xiàn)中荔泳,我們返回一個(gè)新的Signal
蕉饼,為了構(gòu)造這個(gè)Signal
,我們使用初始化方法init(_ creater: (Observer<E>) -> ())
玛歌,在creater
閉包中訂閱舊的Signal
昧港。倘若舊Signal
的Observer
發(fā)出error
事件,則直接把error
事件中關(guān)聯(lián)的Error
實(shí)例提取出來(lái)支子,通過(guò)creater
閉包中作為參數(shù)傳入的Observer
包裹起來(lái)再傳遞出去创肥;而若是舊Signal
的Observer
發(fā)出next
事件,則先把next
關(guān)聯(lián)的數(shù)據(jù)元素提取出來(lái),通過(guò)調(diào)用bind
傳進(jìn)來(lái)的函數(shù)叹侄,獲取一個(gè)中間層的Signal
巩搏,再通過(guò)對(duì)這個(gè)中間層Signal
進(jìn)行訂閱,將事件傳遞到新的Signal
中趾代。
creater
閉包中我使用了[weak self]
捕獲列表來(lái)對(duì)舊Signal
進(jìn)行若引用以防止循環(huán)引用的發(fā)生贯底,為什么這里可能會(huì)發(fā)生循環(huán)引用?上面提到過(guò)撒强,Observer
會(huì)引用Signal
禽捆,而在creater
閉包中舊的Signal
將引用新Signal
的Observer
,從而可以推出舊的Signal
會(huì)對(duì)新Signal
持引用關(guān)系飘哨,這里如果不留意的話會(huì)造成循環(huán)引用胚想。
Monad
中的bind
方法將自動(dòng)處理上下文。在Signal
中芽隆,bind
則幫我們自己處理好事件的訂閱顿仇、轉(zhuǎn)移、傳遞摆马,而我們只需要專(zhuān)注于純數(shù)據(jù)的轉(zhuǎn)換臼闻。
map
方法的實(shí)現(xiàn)十分簡(jiǎn)單,通過(guò)在內(nèi)部調(diào)用bind
方法囤采,并將最終數(shù)據(jù)通過(guò)return
包裹進(jìn)Signal
上下文中述呐,在這里我就不多說(shuō)了削葱。
以上仔粥,我們的響應(yīng)式Monad
就實(shí)現(xiàn)完成了!
以上只是非常簡(jiǎn)單地實(shí)現(xiàn)函數(shù)式響應(yīng)式萨驶,目的是為了簡(jiǎn)單介紹如何利用函數(shù)式編程思想去完成響應(yīng)式的操作代虾,其中并沒(méi)有考慮有關(guān)跨線程調(diào)度的問(wèn)題进肯,大家如果有興趣的可以自己嘗試去進(jìn)行相關(guān)優(yōu)化。
下面我們來(lái)測(cè)試使用一下棉磨。
簡(jiǎn)單使用
通過(guò)creater閉包構(gòu)建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
通過(guò)pipe構(gòu)建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
擴(kuò)展
接下來(lái)我們對(duì)剛剛實(shí)現(xiàn)的函數(shù)式響應(yīng)式進(jìn)行擴(kuò)展江掩,關(guān)聯(lián)一些平時(shí)我們常用到的類(lèi)。
UIControl
對(duì)UIControl
的觸發(fā)事件進(jìn)行監(jiān)聽(tīng)乘瓤,傳統(tǒng)的做法是通過(guò)調(diào)用addTarget(_:, action:, for:)
方法环形,傳入target以及一個(gè)回調(diào)函數(shù)Selector。很多人比較厭倦這種方法衙傀,覺(jué)得每次監(jiān)聽(tīng)事件都需要定義一個(gè)事件處理函數(shù)抬吟,比較麻煩,希望能直接通過(guò)閉包回調(diào)事件觸發(fā)统抬。
這里只需簡(jiǎn)單地封裝一下即可滿足這種需求:
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
對(duì)象來(lái)將UIControl
事件觸發(fā)傳遞到閉包中火本,并通過(guò)關(guān)聯(lián)對(duì)象
來(lái)使得UIControl
保持對(duì)ControlTarget
的引用危队,以防止其被自動(dòng)釋放。經(jīng)過(guò)上面簡(jiǎn)單的封裝后钙畔,我們就能很方面地利用閉包監(jiān)聽(tīng)UIControl
的事件回調(diào):
button.on(events: .touchUpInside) { button in
print("\(button) - TouchUpInside")
}
button.on(events: .touchUpOutside) { button in
print("\(button) - TouchUpOutside")
}
由此茫陆,我們可以簡(jiǎn)單地基于上面的封裝來(lái)擴(kuò)展我們的函數(shù)式響應(yīng)式:
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:)
方法傳入一個(gè)需要進(jìn)行監(jiān)聽(tīng)的事件類(lèi)型,返回一個(gè)Signal
刃鳄,當(dāng)對(duì)應(yīng)的事件觸發(fā)時(shí)盅弛,Signal
中則會(huì)發(fā)射出事件钱骂。而tap
返回的則是針對(duì)TouchUpInside
事件觸發(fā)的Signal
叔锐。
使用起來(lái)跟RxSwift
或ReactiveCocoa
一樣,十分簡(jiǎn)潔優(yōu)雅:
button.tap.map { _ in "Tap~" }.subscribe(next: { message in
print(message)
})
上面整個(gè)過(guò)程的引用關(guān)系為: UIControl -> ControlTarget -> _callback -> Observer -> Signal见秽,由此我們知道愉烙,只要保持對(duì)
UIControl
的引用,那么其所關(guān)聯(lián)的事件監(jiān)聽(tīng)Signal
則不會(huì)被自動(dòng)釋放解取,可以在整個(gè)RunLoop
中持續(xù)工作步责,
NotificationCenter
將函數(shù)式響應(yīng)式適配控制中心,方法跟上面對(duì)UIControl
的擴(kuò)展一樣禀苦,通過(guò)一個(gè)中間層NotificationObserver
來(lái)做事件的傳遞轉(zhuǎn)發(fā):
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可能對(duì)返回的Signal進(jìn)行引用蔓肯,從而造成循環(huán)引用
return Signal { [weak self] observer in
self?.callback(name, object: object, callback: { notification in
observer.sendNext(notification)
})
}
}
}
由此,我們可以基于上面對(duì)NotificationCenter
的響應(yīng)式擴(kuò)展振乏,來(lái)完成對(duì)UITextFiled
文字變化的監(jiān)聽(tīng):
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)
})
方法調(diào)用監(jiān)聽(tīng) / 代理調(diào)用監(jiān)聽(tīng)
我們有時(shí)候想監(jiān)聽(tīng)某個(gè)對(duì)象中指定方法的調(diào)用蔗包,來(lái)實(shí)現(xiàn)面向切面編程或者埋點(diǎn),另外慧邮,當(dāng)函數(shù)式響應(yīng)式被引入后调限,我們希望它能充當(dāng)代理的職責(zé),監(jiān)聽(tīng)代理方法的調(diào)用误澳。為此我們可以通過(guò)對(duì)函數(shù)式響應(yīng)式進(jìn)行擴(kuò)展來(lái)支持上面的需求耻矮。不過(guò)要做這件事情并不簡(jiǎn)單,這里面要涉及多種Runtime
特性忆谓,如方法交換裆装、方法動(dòng)態(tài)派發(fā)、isa交換等Runtime黑科技倡缠,要實(shí)踐它可能需要投入較大精力米母,花費(fèi)較長(zhǎng)時(shí)間。因本人能力與時(shí)間有限毡琉,沒(méi)有去編寫(xiě)相應(yīng)的代碼铁瞒,若大家有興趣可以嘗試一下,而后期如果我做了相關(guān)的努力桅滋,也會(huì)公布出來(lái)慧耍。
為什么沒(méi)有Disposable
若我們接觸過(guò)RxSwift
身辨、ReactiveSwift
,我們會(huì)發(fā)現(xiàn)每次我們訂閱完一個(gè)Observable
或者Signal
后芍碧,會(huì)得到訂閱方法返回的一個(gè)專(zhuān)門(mén)用于回收資源的實(shí)例煌珊,比如RxSwift
中的Disposable
,我們可以通過(guò)在某個(gè)時(shí)機(jī)調(diào)用它的dispose
方法泌豆,或者將其放入一個(gè)DisposeBag
中來(lái)使得資源在最后得到充分的回收定庵。
再來(lái)看回我們?cè)谏厦鎸?shí)現(xiàn)的響應(yīng)式框架,因?yàn)檫@個(gè)框架的實(shí)現(xiàn)非常簡(jiǎn)單踪危,并不會(huì)在訂閱后返回一個(gè)專(zhuān)門(mén)提供給我們釋放資源的實(shí)例蔬浙,所以我們?cè)谑褂盟臅r(shí)候要密切留意資源的存活與釋放問(wèn)題。這里舉一個(gè)例子:
在上面贞远,我們對(duì)函數(shù)式響應(yīng)式進(jìn)行針對(duì)UIControl
的適配時(shí)畴博,是通過(guò)一個(gè)中間層ControlTarget
來(lái)完成的,為了保持這個(gè)ControlTarget
實(shí)例的存活蓝仲,使得它不會(huì)被自動(dòng)釋放俱病,我們先用一個(gè)集合來(lái)包裹住它,并將這個(gè)集合設(shè)置為目標(biāo)UIControl
的關(guān)聯(lián)對(duì)象袱结。此時(shí)我們可以將這個(gè)中間層ControlTarget
看做是這個(gè)事件流管道中的一個(gè)資源亮隙,這個(gè)資源的銷(xiāo)毀是由目標(biāo)UIControl
來(lái)決定的。
對(duì)于RxSwift
來(lái)說(shuō)垢夹,它實(shí)現(xiàn)對(duì)UIControl
的擴(kuò)展原理跟我們寫(xiě)的差不多溢吻,也是通過(guò)一個(gè)中間層來(lái)完成,但是對(duì)于中間層資源的迸锒活與銷(xiāo)毀煤裙,它采用的是另一種方法,我們可以看下這段RxSwift
的源碼(為了簡(jiǎn)單噪漾,刪掉了一些無(wú)關(guān)的代碼):
class RxTarget {
private var retainSelf: RxTarget?
init() {
self.retainSelf = self
}
func dispose() {
self.retainSelf = nil
}
}
這個(gè)類(lèi)型的迸鹋椋活方式十分巧妙,它利用自己對(duì)自己的循環(huán)引用來(lái)使得維持生存欣硼,而當(dāng)調(diào)用dispose
方法時(shí)题翰,它將解開(kāi)對(duì)自己的循環(huán)引用,從而將自己銷(xiāo)毀诈胜。
通過(guò)上面兩個(gè)例子的對(duì)比豹障,我們可以知道,對(duì)于我們自己實(shí)現(xiàn)的響應(yīng)式框架焦匈,我們需要把某些精力放在對(duì)資源的毖活與釋放上,而像RxSwift
缓熟,它則提供一個(gè)統(tǒng)一的資源管理方式累魔,相比起來(lái)更加清晰優(yōu)雅摔笤,大家有興趣可以實(shí)現(xiàn)一下這種方式。
相關(guān)鏈接
Github - ReactiveObjc
Github - ReactiveCocoa
Github - RxSwift
本文純屬個(gè)人見(jiàn)解垦写,若大家發(fā)現(xiàn)文章部分有誤吕世,歡迎在評(píng)論區(qū)提出。