ReactiveCocoa框架菜鳥入門(四)——信號(Signal)詳解

基礎知識

在閱讀本文之前,請確保你已成功導入ReactiveCocoa框架並對信號(Signal)和訂閱者(Subscriber)有基本瞭解。或者嘗試着完全理解以下一段內容:

信號是數據流,可以被綁定和傳遞。可以把信號想象成水龍頭,只不過裏面不是水,而是玻璃球(value),直徑跟水管的內徑一樣,這樣就能保證玻璃球是依次排列,不會出現並排的情況(數據都是線性處理的,不會出現併發情況)。水龍頭的開關默認是關的,除非有了接收方(subscriber),纔會打開。這樣只要有新的玻璃球進來,就會自動傳送給接收方。接收方就是放在水龍頭下的盆子,對於水龍頭不同的出水狀況,有自己的處理方式。水龍頭出水時會通知下方的水盆,如果沒有水盆,水龍頭始終處於關閉狀態。

(轉自linyawen的博客,附加了一些個人總結。)

再看信號

在前文中,以UITextfield的信號爲例,示範了信號(Signal)的基本使用。但是,顯然信號(Signal)的功能遠不止這些。本文將詳細介紹對信號進行的一系列操作。

首先,作爲一個信號,我們關注它的兩個方面:

  1. 處理邏輯
  2. 數據內容

處理邏輯指的是創建信號的時候,它是如何通知訂閱者(Subscriber)並選擇發送何種事件的。數據內容指的是信號會傳遞給訂閱者(Subscriber)什麼樣的數據。這就像一個水龍頭,它什麼時候告訴水盆自己正在滴水,或是已經滴完水了。以及它把什麼丟入水盆,是原始的水滴,還是水滴的質量?

如果我們需要對這些內容進行自定義的修改,那麼修改原信號顯然是不可行的(信號已經被創建了)。因此,這就牽涉到信號之間的轉換(Map)與組合(Combine)。

我們從RACSignal類最基礎的方法開始討論信號之間的轉換(Map)與組合(Combine)。對於每一個方法,我們需要關注這個方法的功能、返回值類型。由於ReactiveCocoa大量使用了block,還需要關注方法中block的參數類型和返回值類型。

綁定(Bind)

在RACSignal.m中找到bind方法。官方定義如下:

/* 
* -bind: should: 
*  
* 1. Subscribe to the original signal of values. 
* 2. Any time the original signal sends a value, transform it using the binding block. 
* 3. If the binding block returns a signal, subscribe to it, and pass all of its values through to the subscriber as they’re received. 
* 4. If the binding block asks the bind to terminate, complete the original signal. 
* 5. When all signals complete, send completed to the subscriber. 
*  
* If any signal sends an error at any point, send that to the subscriber. 
*/

觀察bind方法的實現(太長了,就不貼出來了)顯然這個方法返回了一個新的信號。定義1表示會訂閱原始信號,這樣原始信號一定是熱信號(Hot Signal)。定義4、5告訴我們新的信號發送事件和原始信號是同步的。這是bind方法最重要的特點之一。 
同樣需要注意的是方法的第一行代碼:

<code class="hljs fix has-numbering" style="display: block; padding: 0px; background-color: transparent; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background-position: initial initial; background-repeat: initial initial;"><span class="hljs-attribute" style="box-sizing: border-box;">RACStreamBindBlock bindingBlock </span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> block();</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

這裏的block不再是此前我們簡單認爲的block。這個block被調用後,纔得到一個block。這是一個block的嵌套。注意到後半段一行代碼:

<code class="hljs fix has-numbering" style="display: block; padding: 0px; background-color: transparent; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background-position: initial initial; background-repeat: initial initial;"><span class="hljs-attribute" style="box-sizing: border-box;">id signal </span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> bindingBlock(x, &stop);</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

這裏表明,通過解封出來的block,產生一個新的signal。

因此,bind方法的作用大概已經清楚了:通過傳入一個block(解除一層嵌套後)作用於原始信號上,產生一個新的信號。這個新的信號與原始信號保持同步。

FlattenMap

沒辦法翻譯這個方法。但是它確實信號非常重要的一個方法。網上很多教程先介紹Map方法再把FlattenMap作爲Map的補充介紹,這個邏輯是不正確的。觀察源碼不難發現,綁定(Bind)屬於最底層操作,核心是保持了新舊信號的同步性和創造了對原始信號進行修改的可能。而FlattenMap初步提供了修改的機制。

觀察FlattenMap方法的實現代碼:

<code class="hljs applescript has-numbering" style="display: block; padding: 0px; background-color: transparent; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background-position: initial initial; background-repeat: initial initial;">- (instancetype)flattenMap:(RACStream * (^)(<span class="hljs-property" style="box-sizing: border-box;">id</span> value))block {
    Class <span class="hljs-type" style="box-sizing: border-box;">class</span> = self.<span class="hljs-type" style="box-sizing: border-box;">class</span>;
<span class="hljs-command" style="box-sizing: border-box;">
    return</span> [[self bind:^{
<span class="hljs-command" style="box-sizing: border-box;">        return</span> ^(<span class="hljs-property" style="box-sizing: border-box;">id</span> value, BOOL *stop) {
            <span class="hljs-property" style="box-sizing: border-box;">id</span> stream = block(value) ?: [<span class="hljs-type" style="box-sizing: border-box;">class</span> empty];
            NSCAssert([stream isKindOfClass:RACStream.<span class="hljs-type" style="box-sizing: border-box;">class</span>], @<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"Value returned from -flattenMap: is not a stream: %@"</span>, stream);
<span class="hljs-command" style="box-sizing: border-box;">
            return</span> stream;
        };
    }] setNameWithFormat:@<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"[%@] -flattenMap:"</span>, self.<span class="hljs-property" style="box-sizing: border-box;">name</span>];
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li></ul>

最後的setNameWithFormat顯然是一個格式化輸出,並不影響信號的本質。直接無視它,那麼這個方法的核心其實就是return [self bind:^{}];即返回了一個綁定了原始信號的新信號。bind方法的block中的return方法將在RACStreamBindBlock bindingBlock = block();時被調用,相當於解除了嵌套。實際上作用於原始信號的代碼就是被return的那個block中的代碼。

因此不難看出,FlattenMap方法的作用在於通過傳入一個block,作用在原始信號傳出的value上,得到一個新的信號。很抱歉我不明白一個blcok作用在value上能得到什麼新的信號,但是實際使用中的情況是,這個value作爲參數被傳入blcok中,但是block完全沒有用到這個參數,而是自己創建了一個信號。

相比於綁定(Bind)側重於新信號和原始信號的同步性,FlattenMap方法實現了新 信號的修改。綁定(Bind)屬於最底層操作,而FlattenMap方法是中間層,爲實際應用提供了一個接口。當然有時候FlattenMap方法也會被我們直接調用。

信號(Signal)的各種操作

在之前的基礎上,ReactiveCocoa提供了對信號的各種操作。這些操作幾乎都用到了FlattenMap方法。意味着返回一個被修改之後的信號。同時,幾乎每個操作還調用了return方法。

<code class="hljs cs has-numbering" style="display: block; padding: 0px; background-color: transparent; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background-position: initial initial; background-repeat: initial initial;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//這個return不是我們用於返回一個值的return,只是名字比較像。</span>
+ (RACSignal *)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span>:(id)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">value</span> {
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> [RACReturnSignal <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span>:<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">value</span>];
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>

這個方法涉及到的代碼比較多,就不一一細講。該方法的主要作用是,返回一個新的信號,不過原始信號發送事件時的value將被新的value替換。 
有了對綁定(Bind)方法、FlattenMap方法和return方法的理解,基本上就可以通過自己閱讀源碼搞定對信號(Signal)的各種操作了。這裏列出幾個常用的操作,如果依然不能理解,或者想要了解更多操作還是建議直接閱讀源碼。

filter 
filter方法返回一個新的signal。原始信號的value被替換爲了符合要求的value,從而實現了篩選、過濾的目的。是否符合要求是由傳入的block決定的。即原來的信號的value,如果傳入block中返回YES,則新的信號也將輸出這個value。

map 
map方法返回一個新的signal。原始信號的value被替換爲了經過block處理的value。

distinctUntilChanged 
distinctUntilChanged方法返回一個新的signal。這個signal只在value和前一個value不同的時候纔會發送事件。簡記爲求異存同。

ignore 
這個方法需要傳入一個value,當信號收到一個value時,會檢查是否和傳入的value相同,如果相同就不會發送事件給訂閱者。

skip & take 
顧名思義,就是跳過(只發送)前n條數據。這裏的n就是傳入的參數值。

doNext 
創建一個新的信號,這個信號和原始信號一模一樣,不過可以在創建的過程中調用傳入的block。

combineLatest:reduce 
合併若干個信號,得到一個新的信號。把那些信號的value進行處理,得到一個處理過後的value作爲新的信號的value。

throttle 
throttle方法返回一個新的signal。只有在給定時間原始信號沒有發送next事件,這個信號纔會發送一個原始信號最近的一次next事件。

通過對信號的各種操作,我們把若干個水龍頭連在一起,形成了一個水管。filter像是在兩個水龍頭之間加了一個過濾網,只有經過過濾網的水才能出現在下一個水龍頭裏。map像是在水龍頭間加了一個轉換器,前一個水龍頭流出的水經過這個轉換器就變成石油了。combineLatest:reduce則是把若干個水龍頭的水一起引入一個新的水龍頭……

以上是常用的信號(Signal)操作,更多的操作可以在源代碼中找到,相信有了之前的基礎,看懂這些代碼並不困難。現在我們已經有了足夠多的辦法處理一個信號,開始實際編程工作已經不是問題了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章