ReactiveCocoa 之 優雅的 RACCommand

RACCommand 是一個在 ReactiveCocoa 中比較複雜的類,大多數使用 ReactiveCocoa 的人,尤爲是初學者並不會常用它。react

在不少狀況下,雖然使用 RACSignalRACSubject 就能解決絕大部分問題,可是 RACCommand 的使用會爲咱們帶來巨大的便利,尤爲是在與反作用相關的操做中。git

 
What-is-RACCommand

文章中不會討論 RACCommand 中的並行執行問題,也就是忽略了 allowsConcurrentExecution 以及 allowsConcurrentExecutionSubject 的存在,不過它們確實在 RACCommand 中很是重要,這裏只是爲了減小沒必要要的干擾因素。github

RACCommand 簡介

與前面幾篇文章中介紹的 RACSignal 等元素不一樣,RACCommand 並不表示數據流,它只是一個繼承自 NSObject 的類,可是它卻能夠用來建立和訂閱用於響應某些事件的信號。api

@interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject @end 

它自己並非一個 RACStream 或者 RACSignal 的子類,而是一個用於管理 RACSignal 的建立與訂閱的類。服務器

在 ReactiveCocoa 中的 FrameworkOverview 部分對 RACCommand 有這樣的解釋:網絡

A command, represented by the RACCommand class, creates and subscribes to a signal in response to some action. This makes it easy to perform side-effecting work as the user interacts with the app.併發

在用於與 UIKit 組件進行交互或者執行包含反作用的操做時,RACCommand 可以幫助咱們更快的處理而且響應任務,減小編碼以及工程的複雜度。app

RACCommand 的初始化與執行

-initWithSignalBlock: 方法的方法簽名上,你能夠看到在每次 RACCommand 初始化時都會傳入一個類型爲 RACSignal<ValueType> * (^)(InputType _Nullable input)signalBlockide

- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

輸入爲 InputType 返回值爲 RACSignal<ValueType> *,而 InputType 也就是在調用 -execute: 方法時傳入的對象:函數

- (RACSignal<ValueType> *)execute:(nullable InputType)input;

這也就是 RACCommand 將外部變量(或『反作用』)傳入 ReactiveCocoa 內部的方法,你能夠理解爲 RACCommand 將外部的變量 InputType 轉換成了使用 RACSignal 包裹的 ValueType 對象。

 
Execute-For-RACCommand

咱們如下面的代碼爲例,先來看一下 RACCommand 是如何工做的:

RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(NSNumber * _Nullable input) {
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        NSInteger integer = [input integerValue];
        for (NSInteger i = 0; i < integer; i++) {
            [subscriber sendNext:@(i)];
        }
        [subscriber sendCompleted];
        return nil;
    }];
}];
[[command.executionSignals switchToLatest] subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

[command execute:@1];
[RACScheduler.mainThreadScheduler afterDelay:0.1
                                    schedule:^{
                                        [command execute:@2];
                                    }];
[RACScheduler.mainThreadScheduler afterDelay:0.2
                                    schedule:^{
                                        [command execute:@3];
                                    }];

首先使用 -initWithSignalBlock: 方法建立一個 RACCommand 的對象,傳入一個類型爲 InputType -> RACSignal<ValueType> 的 block,這個信號根據輸入會發送對應次數的消息,若是運行上面的代碼,會打印出:

0
0
1
0
1
2

-switchToLatest 方法只能操做信號的信號

每次 executionSignals 中發送了新的信號時,switchToLatest 方法返回的信號都會訂閱這個最新的信號,這裏也就保證了每次都會打印出最新的信號中的值。

 
Multiple-Executes

在上面代碼中還有最後一個問題須要回答,爲何要使用 RACScheduler.mainThreadScheduler 延遲調用以後的 -execute: 方法?因爲在默認狀況下 RACCommand 都是不支持併發操做的,須要在上一次命令執行以後才能夠發送下一次操做,不然就會返回錯誤信號 RACErrorSignal,這些錯誤能夠經過訂閱 command.errors 得到。

若是使用以下的方式執行幾回 -execute: 方法:

[command execute:@1];
[command execute:@2];
[command execute:@3];

筆者相信,不出意外的話,你只能在控制檯中看到輸出 0

最重要的內部『信號』

RACCommand 中最重要的內部『信號』就是 addedExecutionSignalsSubject

@property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject;

這個 RACSubject 對象經過各類操做衍生了幾乎全部 RACCommand 中的其餘信號,咱們會在下一節中具體介紹;

既然 addedExecutionSignalsSubject 是一個 RACSubject,它不能在建立時預設好對訂閱者發送的消息,它會在哪裏接受數據並推送給訂閱者呢?答案就在 -execute: 方法中:

- (RACSignal *)execute:(id)input {
    BOOL enabled = [[self.immediateEnabled first] boolValue];
    if (!enabled) {
        NSError *error = [NSError errorWithDomain:RACCommandErrorDomain code:RACCommandErrorNotEnabled userInfo:@{
            NSLocalizedDescriptionKey: NSLocalizedString(@"The command is disabled and cannot be executed", nil),
            RACUnderlyingCommandErrorKey: self
        }];

        return [RACSignal error:error];
    }

    RACSignal *signal = self.signalBlock(input);
    RACMulticastConnection *connection = [[signal
        subscribeOn:RACScheduler.mainThreadScheduler]
        multicast:[RACReplaySubject subject]];
    
    [self.addedExecutionSignalsSubject sendNext:connection.signal];

    [connection connect];
    return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, RACDescription(input)];
}

在方法中這裏你也能看到連續幾回執行 -execute: 方法不能成功的緣由:每次執行這個方法時,都會從另外一個信號 immediateEnabled 中讀取是否能執行當前命令的 BOOL 值,若是不能夠執行的話,就直接返回 RACErrorSignal

 
Execute-on-RACCommand

-execute: 方法是惟一一個爲 addedExecutionSignalsSubject 生產信息的方法。

在執行 signalBlock 返回一個 RACSignal 以後,會將當前信號包裝成一個 RACMulticastConnection,而後調用 -sendNext: 方法發送到 addedExecutionSignalsSubject 上,執行 -connect 方法訂閱原有的信號,最後返回。

複雜的初始化

與簡單的 -execute: 方法相比,RACCommand 的初始化方法就複雜多了,雖然咱們在方法中傳入了 signalBlock,可是 -initWithEnabled:signalBlock: 方法只是對這個 block 進行了簡單的 copy,真正使用這個 block 的仍是上一節中的 -execute: 方法中。

因爲 RACCommand 在初始化方法中初始化了七個高階信號,它的實現很是複雜:

- (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal<id> * (^)(id input))signalBlock {
    self = [super init];

    _addedExecutionSignalsSubject = [RACSubject new];
    _signalBlock = [signalBlock copy];

    _executionSignals = ...;
    _errors = ...;
    RACSignal *immediateExecuting = ...;
    _executing = ...;
    RACSignal *moreExecutionsAllowed = ...;
    _immediateEnabled =...;
    _enabled = ...;

    return self;
}

這一小節並不能徹底介紹所有的七個信號的實現,只會介紹其中的 immediateExecutingmoreExecutionsAllowed 兩個臨時信號,剩下的信號都會在下一節中分析。

表示當前有操做執行的信號

首先是 immediateExecuting 信號:

RACSignal *immediateExecuting = [[[[self.addedExecutionSignalsSubject
    flattenMap:^(RACSignal *signal) {
        return [[[signal
            catchTo:[RACSignal empty]]
            then:^{
                return [RACSignal return:@-1];
            }]
            startWith:@1];
    }]
    scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) {
        return @(running.integerValue + next.integerValue);
    }]
    map:^(NSNumber *count) {
        return @(count.integerValue > 0);
    }]
    startWith:@NO];

immediateExecuting 是一個用於表示當前是否有任務執行的信號,若是輸入的 addedExecutionSignalsSubject 等價於如下的信號:

[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:[RACSignal error:[NSError errorWithDomain:@"Error" code:1 userInfo:nil]]];
    [subscriber sendNext:[RACSignal return:@1]];
    [subscriber sendNext:[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [RACScheduler.mainThreadScheduler afterDelay:1
                                            schedule:^
         {
             [subscriber sendCompleted];
         }];
        return nil;
    }]];
    [subscriber sendNext:[RACSignal return:@3]];
    [subscriber sendCompleted];
    return nil;
}];

在本文的全部章節中都會假設輸入的 addedExecutionSignalsSubject 信號跟上面的代碼返回的徹底相同。

那麼,最後生成的高階信號 immediateExecuting 以下:

 
immediateExecuting-Signal-in-RACCommand
  1. -catchTo: 將全部的錯誤轉換成 RACEmptySignal 信號;
  2. -flattenMap: 將每個信號的開始和結束的時間點轉換成 1-1 兩個信號;
  3. -scanWithStart:reduce:0 開始累加原有的信號;
  4. -map: 將大於 1 的信號轉換爲 @YES
  5. -startWith: 在信號序列最前面加入 @NO,表示在最開始時,沒有任何動做在執行。

immediateExecuting 使用幾個 RACSignal 的操做成功將原有的信號流轉換成了表示是否有操做執行的信號流。

表示是否容許更多操做執行的信號

相比於 immediateExecuting 信號的複雜,moreExecutionsAllowed 就簡單多了:

RACSignal *moreExecutionsAllowed = [RACSignal
    if:[self.allowsConcurrentExecutionSubject startWith:@NO]
    then:[RACSignal return:@YES]
    else:[immediateExecuting not]];

由於文章中不許備介紹與併發執行有關的內容,因此這裏的 then 語句永遠不會執行,既然 RACCommand 不支持並行操做,那麼這段代碼就很是好理解了,當前 RACCommand 可否執行操做就是 immediateExecuting 取反:

 
MoreExecutionAllowed-Signa

到這裏全部初始化方法中的臨時信號就介紹完了,在下一節中會繼續介紹初始化方法中的其它高階信號。

RACCommand 接口中的高階信號

每個 RACCommand 對象中都管理着多個信號,它在接口中暴露出的四個信號是這一節關注的重點:

 
RACCommand-Interface

這一小節會按照順序圖中從上到下的順序介紹 RACCommand 接口中暴露出來的信號,同時會涉及一些爲了生成這些信號的中間產物。

executionSignals

executionSignalsRACCommand 中最重要的信號;從類型來看,它是一個包含信號的信號,在每次執行 -execute: 方法時,最終都會向 executionSignals 中傳入一個最新的信號。

雖然它最重要,可是executionSignals 是這個幾個高階信號中實現最簡單的:

_executionSignals = [[[self.addedExecutionSignalsSubject
    map:^(RACSignal *signal) {
        return [signal catchTo:[RACSignal empty]];
    }]
    deliverOn:RACScheduler.mainThreadScheduler]
    setNameWithFormat:@"%@ -executionSignals", self];

它只是將信號中的全部的錯誤 NSError 轉換成了 RACEmptySignal 對象,並派發到主線程上。

 
Execution-Signals

若是你只訂閱了 executionSignals,那麼其實你不會收到任何的錯誤,全部的錯誤都會以 -sendNext: 的形式被髮送到 errors 信號中,這會在後面詳細介紹。

executing

executing 是一個表示當前是否有任務執行的信號,這個信號使用了在上一節中介紹的臨時變量做爲數據源:

_executing = [[[[[immediateExecuting
    deliverOn:RACScheduler.mainThreadScheduler]
    startWith:@NO]
    distinctUntilChanged]
    replayLast]
    setNameWithFormat:@"%@ -executing", self];

 

這裏對 immediateExecuting 的變換仍是很是容易理解的:

 
Executing-Signa

最後的 replayLast 方法將原有的信號變成了容量爲 1RACReplaySubject 對象,這樣在每次有訂閱者訂閱 executing 信號時,都只會發送最新的狀態,由於訂閱者並不關心過去的 executing 的值。

enabled

enabled 信號流表示當前的命令是否能夠再次被執行,也就是 -execute: 方法可否能夠成功執行新的任務;該信號流依賴於另外一個私有信號 immediateEnabled

RACSignal *enabledSignal = [RACSignal return:@YES];

_immediateEnabled = [[[[RACSignal
    combineLatest:@[ enabledSignal, moreExecutionsAllowed ]]
    and]
    takeUntil:self.rac_willDeallocSignal]
    replayLast];

雖然這個信號的實現比較簡單,不過它同時與三個信號有關,enabledSignalmoreExecutionsAllowed 以及 rac_willDeallocSignal

 
Immediate-Enabled-Signa

雖然圖中沒有體現出方法 -takeUntil:self.rac_willDeallocSignal 的執行,不過你須要知道,這個信號在當前 RACCommand 執行 dealloc 以後就不會再發出任何消息了。

enabled 信號其實與 immediateEnabled 相差無幾:

_enabled = [[[[[self.immediateEnabled
    take:1]
    concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]]
    distinctUntilChanged]
    replayLast]
    setNameWithFormat:@"%@ -enabled", self];

從名字你能夠看出來,immediateEnabled 在每次原信號發送消息時都會從新計算,而 enabled 調用了 -distinctUntilChanged 方法,因此若是連續幾回值相同就不會再次發送任何消息。

除了調用 -distinctUntilChanged 的區別以外,你能夠看到 enabled 信號在最開始調用了 -take:-concat: 方法:

[[self.immediateEnabled take:1] concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]] 

雖然序列並無任何的變化,可是在這種狀況下,enabled 信號流中的第一個值會在訂閱線程上到達,剩下的全部的值都會在主線程上派發;若是你知道,在通常狀況下,咱們都會使用 enabled 信號來控制 UI 的改變(例如 UIButton),相信你就會明白這麼作的理由了。

errors

錯誤信號是 RACCommand 中比較簡單的信號;爲了保證 RACCommand 對此執行 -execute: 方法也能夠繼續運行,咱們只能將全部的錯誤以其它的形式發送到 errors 信號中,防止向 executionSignals 發送錯誤信號後,executionSignals 信號就會停止的問題。

咱們使用以下的方式建立 errors 信號:

RACMulticastConnection *errorsConnection = [[[self.addedExecutionSignalsSubject
    flattenMap:^(RACSignal *signal) {
        return [[signal
            ignoreValues]
            catch:^(NSError *error) {
                return [RACSignal return:error];
            }];
    }]
    deliverOn:RACScheduler.mainThreadScheduler]
    publish];

_errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self];
[errorsConnection connect];

信號的建立過程是把全部的錯誤消息從新打包成
並在主線程上進行派發:RACErrorSignal
 
Errors-Signals

使用者只須要調用 -subscribeNext: 就能夠從這個信號中獲取全部執行過程當中發生的錯誤。

RACCommand 的使用

RACCommand 很是適合封裝網絡請求,咱們可使用下面的代碼封裝一個網絡請求:

RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        NSURL *url = [NSURL URLWithString:@"http://localhost:3000"];
        AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
        NSString *URLString = [NSString stringWithFormat:@"/api/products/%@", input ?: @1];
        NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil
             success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
                 [subscriber sendNext:responseObject];
                 [subscriber sendCompleted];
             } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                 [subscriber sendError:error];
             }];
        return [RACDisposable disposableWithBlock:^{
            [task cancel];
        }];
    }];
}];

上面的 RACCommand 對象能夠經過 -execute: 方法執行,同時,訂閱 executionSignals 以及 errors 來獲取網絡請求的結果。

[[command.executionSignals switchToLatest] subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];
[command.errors subscribeNext:^(NSError * _Nullable x) {
    NSLog(@"%@", x);
}];
[command execute:@1];

向方法 -execute: 中傳入了 @1 對象,從服務器中獲取了 id = 1 的商品對象;固然,咱們也能夠傳入不一樣的 id 來獲取不一樣的模型,全部的網絡請求以及 JSON 轉換模型的邏輯均可以封裝到這個 RACCommand 的 block 中,外界只是傳入一個 id,最後就從 executionSignals 信號中獲取了開箱即用的對象。

總結

使用 RACCommand 可以優雅地將包含反作用的操做和與反作用無關的操做分隔起來;整個 RACCommand 至關於一個黑箱,從 -execute: 方法中得到輸入,最後以向信號發送消息的方式,向訂閱者推送結果。

 
RACCommand-Side-Effect

這種執行任務的方式就像是一個函數,根據輸入的不一樣,有着不一樣的輸出,很是適合與 UI、網絡操做的相關的任務,這也是 RACCommand 的設計的優雅之處。

References

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/raccommand

做者:Draveness 連接:https://www.jianshu.com/p/ae71313f5846 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索