函數(shù)式編程 - 實(shí)現(xiàn)響應(yīng)式框架

前言

函數(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)始你的表演了。

請(qǐng)開(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)換思想的概念圖:

流轉(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中的bindreturn函數(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)儒将,它僅具有nexterror類(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)了valuenext事件,這樣做是為了保證整個(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)自于Signalaction(_:)方法,這個(gè)方法引用到了Signal中的成員屬性池凄。由此抡驼,我們可以推出此時(shí)Observer對(duì)Signal具有引用的關(guān)系,Observer不釋放肿仑,Signal也會(huì)一直保留致盟。

接下來(lái)就是讓Signal實(shí)現(xiàn)Monadbind方法了:

// 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í)跟ReactiveCocoaflattenMap或是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昧港。倘若舊SignalObserver發(fā)出error事件,則直接把error事件中關(guān)聯(lián)的Error實(shí)例提取出來(lái)支子,通過(guò)creater閉包中作為參數(shù)傳入的Observer包裹起來(lái)再傳遞出去创肥;而若是舊SignalObserver發(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將引用新SignalObserver,從而可以推出舊的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)跟RxSwiftReactiveCocoa一樣,十分簡(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ū)提出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末梯投,一起剝皮案震驚了整個(gè)濱河市命辖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌分蓖,老刑警劉巖尔艇,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異咆疗,居然都是意外死亡漓帚,警方通過(guò)查閱死者的電腦和手機(jī)母债,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)午磁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人毡们,你說(shuō)我怎么就攤上這事迅皇。” “怎么了衙熔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵登颓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我红氯,道長(zhǎng)框咙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任痢甘,我火速辦了婚禮喇嘱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘塞栅。我一直安慰自己者铜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布放椰。 她就那樣靜靜地躺著作烟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砾医。 梳的紋絲不亂的頭發(fā)上拿撩,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音如蚜,去河邊找鬼压恒。 笑死头滔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涎显。 我是一名探鬼主播坤检,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼期吓!你這毒婦竟也來(lái)了早歇?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤讨勤,失蹤者是張志新(化名)和其女友劉穎箭跳,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體潭千,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谱姓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刨晴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屉来。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖狈癞,靈堂內(nèi)的尸體忽然破棺而出茄靠,到底是詐尸還是另有隱情,我是刑警寧澤蝶桶,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布慨绳,位于F島的核電站,受9級(jí)特大地震影響真竖,放射性物質(zhì)發(fā)生泄漏脐雪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一恢共、第九天 我趴在偏房一處隱蔽的房頂上張望战秋。 院中可真熱鬧,春花似錦旁振、人聲如沸获询。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吉嚣。三九已至,卻和暖如春蹬铺,著一層夾襖步出監(jiān)牢的瞬間尝哆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工甜攀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秋泄,地道東北人琐馆。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像恒序,于是被迫代替她去往敵國(guó)和親瘦麸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容