iOS 如何優雅的處理「回調地獄Callback hell」(一) (上)

前言ios

 

最近看了一些Swift關於封裝異步操做過程的文章,好比RxSwift,RAC等等,由於回調地獄我本身也寫過,頗有感觸,因而就翻出了Promise來研究學習一下。現將本身的一些收穫分享一下,有錯誤歡迎你們多多指教。編程

 

目錄json

 

  • 1.PromiseKit簡介swift

  • 2.PromiseKit安裝和使用api

  • 3.PromiseKit主要函數的使用方法數組

  • 4.PromiseKit的源碼解析promise

  • 5.使用PromiseKit優雅的處理回調地獄ruby

 

一.PromiseKit簡介app

 

PromiseKit是iOS/OS X 中一個用來出來異步編程框架。這個框架是由Max Howell(Mac下Homebrew的做者,傳說中由於」不會」寫反轉二叉樹而沒有拿到Google offer)大神級人物開發出來的。框架

 

在PromiseKit中,最重要的一個概念就是Promise的概念,Promise是異步操做後的future的一個值。

 

A promise represents the future value of an asynchronous task.

A promise is an object that wraps an asynchronous task

 

Promise也是一個包裝着異步操做的一個對象。使用PromiseKit,可以編寫出整潔,有序的代碼,邏輯簡單的,將Promise做爲參數,模塊化的從一個異步任務到下一個異步任務中去。用PromiseKit寫出的代碼就是這樣:

 

[self login].then(^{

 

     // our login method wrapped an async task in a promise

     return [API fetchData];

 

}).then(^(NSArray *fetchedData){

 

     // our API class wraps our API and returns promises

     // fetchedData returned a promise that resolves with an array of data

     self.datasource = fetchedData;

     [self.tableView reloadData];

 

}).catch(^(NSError *error){

 

     // any errors in any of the above promises land here

     [[[UIAlertView alloc] init…] show];

 

});

 

PromiseKit就是用來乾淨簡潔的代碼,來解決異步操做,和奇怪的錯誤處理回調的。它將異步操做變成了鏈式的調用,簡單的錯誤處理方式。

 

PromiseKit裏面目前有2個類,一個是Promise<T>(Swift),一個是AnyPromise(Objective-C),2者的區別就在2種語言的特性上,Promise<T>是定義精確嚴格的,AnyPromise是定義寬鬆,靈活,動態的。

 

在異步編程中,有一個最最典型的例子就是回調地獄CallBack hell,要是處理的不優雅,就會出現下圖這樣:

 

 

上圖的代碼是真實存在的,也是朋友告訴個人,來自快的的代碼,固然如今人家確定改掉了。雖然這種代碼看着像這樣:

 

 

代碼雖然看上去不優雅,功能都是正確的,可是這種代碼基本你們都本身寫過,我本身也寫過不少。今天就讓咱們動起手來,用PromiseKit來優雅的處理掉Callback hell吧。

 

二.PromiseKit安裝和使用

 

1.下載安裝CocoaPods

 

在牆外的安裝步驟:

在Terminal裏面輸入

 

sudo gem install cocoapods && pod setup

 

大多數在牆內的同窗應該看以下步驟了:

 

//移除原有的牆外Ruby 默認源

$ gem sources --remove https://rubygems.org/

//添加現有的牆內的淘寶源

$ gem sources -a https://ruby.taobao.org/

//驗證新源是否替換成功

$ gem sources -l

//下載安裝cocoapods

// OS 10.11以前

$ sudo gem install cocoapods

//mark:OS 升級 OS X EL Capitan 後命令應該爲:

$ sudo gem install -n /usr/local/bin cocoapods

//設置cocoapods

$ pod setup

 

2.找到項目的路徑,進入項目文件夾下面,執行:

 

$ touch Podfile && open -e Podfile

 

此時會打開TextEdit,而後輸入一下命令:

 

platform:ios,7.0’

 

target 'PromisekitDemo' do  //因爲最新版cocoapods的要求,因此必須加入這句話

    pod 'PromiseKit'

end

 

Tips:感謝qinfensky大神提醒,其實這裏也能夠用init命令

Podfile是CocoaPods的特殊文件,在其中能夠列入在項目中想要使用的開源庫,若想建立Podfile,有2種方法:

1.在項目目錄中建立空文本文件,命名爲Podfile

2.或者能夠再項目目錄中運行「$ pod init 「,來建立功能性文件(終端中輸入cd 文件夾地址,而後再輸入 pod init)

兩種方法均可以建立Podfile,使用你最喜歡使用的方法

 

3.安裝PromiseKit

 

$ pod install

 

安裝完成以後,退出終端,打開新生成的.xcworkspace文件便可

 

三.PromiseKit主要函數的使用方法

 

1.then

常常咱們會寫出這樣的代碼:

 

- (void)showUndoRedoAlert:(UndoRedoState *)state

{

     UIAlertView *alert = [[UIAlertView alloc] initWithTitle:……];

     alert.delegate = self;

     self.state = state;

     [alert show];

}

 

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex

{

    if (buttonIndex == 1) {

        [self.state do];

    }

 

}

 

上面的寫法也不是錯誤的,就是它在調用函數中保存了一個屬性,在調用alertView會使用到這個屬性。其實這個中間屬性是不須要存儲的。接下來咱們就用then來去掉這個中間變量。

 

- (void)showUndoRedoAlert:(UndoRedoState *)state

{

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:……];

    [alert promise].then(^(NSNumber *dismissedButtonIndex){

        [state do];

    });

}

 

這時就有人問了,爲啥能調用 alert promise 這個方法?後面點語法跟着then是什麼?我來解釋一下,緣由其實只要打開Promise源碼就一清二楚了。在pormise源碼中

 

@interface UIAlertView (PromiseKit)

 

/**

Displays the alert view.

 

@return A promise the fulfills with two parameters:

1) The index of the button that was tapped to dismiss the alert.

2) This alert view.

*/

- (PMKPromise *)promise;

 

對應的實現是這樣的

 

- (PMKPromise *)promise {

    PMKAlertViewDelegater *d = [PMKAlertViewDelegater new];

    PMKRetain(d);

    self.delegate = d;

    [self show];

    return [PMKPromise new:^(id fulfiller, id rejecter){

        d->fulfiller = fulfiller;

    }];

}

 

調用 alert promise 返回仍是一個promise對象,在promise的方法中有then的方法,因此上面能夠那樣鏈式的調用。上面代碼裏面的fulfiller放在源碼分析裏面去講講。

 

在PromiseKit裏面,其實就默認給你建立了幾個類的延展,以下圖

 

 

這些擴展類裏面就封裝了一些經常使用的生成promise方法,調用這些方法就能夠愉快的一路.then執行下去了!

 

2.dispatch_promise

項目中咱們常常會異步的下載圖片

 

typedefvoid(^onImageReady) (UIImage* image);

 

+ (void)getImageWithURL:(NSURL *)url onCallback:(onImageReady)callback

{

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);

    dispatch_async(queue, ^{

        NSData * imageData = [NSData dataWithContentsOfURL:url];

        dispatch_async(dispatch_get_main_queue(), ^{

            UIImage *image = [UIImage imageWithData:imageData];

            callback(image);

        });

    });

}

 

使用dispatch_promise,咱們能夠將它改變成下面這樣:

 

dispatch_promise(^{

        return [NSData dataWithContentsOfURL:url];    

    }).then(^(NSData * imageData){

        self.imageView.image = [UIImage imageWithData:imageData];  

    }).then(^{

        // add code to happen next here

    });

 

咱們看看源碼,看看調用的異步過程對不對

 

- (PMKPromise *(^)(id))then {

    return ^(id block){

        return self.thenOn(dispatch_get_main_queue(), block);

    };

}

 

PMKPromise *dispatch_promise(id block) {

    return dispatch_promise_on(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);

}

 

看了源碼就知道上述是正確的。

 

3.catch

在異步操做中,處理錯誤也是一件很頭疼的事情,以下面這段代碼,每次異步請求回來都必需要處理錯誤。

 

void (^errorHandler)(NSError *) = ^(NSError *error) {

    [[UIAlertView] show];

};

[NSURLConnection sendAsynchronousRequest:rq queue:q completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

    if (connectionError) {

        errorHandler(connectionError);

    } else {

        NSError *jsonError = nil;

        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];

        if (jsonError) {

            errorHandler(jsonError);

        } else {

            id rq = [NSURLRequest requestWithURL:[NSURL URLWithString:json[@"avatar_url"]]];

            [NSURLConnection sendAsynchronousRequest:rq queue:q completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

                UIImage *image = [UIImage imageWithData:data];

                if (!image) {

                    errorHandler(nil); // NSError TODO!

                } else {

                    self.imageView.image = image;

                }

            }];

        }

    }

}];

 

咱們能夠用promise的catch來解決上面的錯誤處理的問題

 

//oc版

[NSURLSession GET:url].then(^(NSDictionary *json){

    return [NSURLConnection GET:json[@"avatar_url"]];

}).then(^(UIImage *image){

    self.imageView.image = image;

}).catch(^(NSError *error){

    [[UIAlertView] show];

})

 

//swift版

firstly {

    NSURLSession.GET(url)

}.then { (json: NSDictionary) in

    NSURLConnection.GET(json["avatar_url"])

}.then { (image: UIImage) in

    self.imageView.image = image

}.error { error in

    UIAlertView(…).show()

}

 

總結起來就是上圖,pending狀態的promise對象既可轉換爲帶着一個成功值的 fulfilled 狀態,也可變爲帶着一個 error 信息的 rejected 狀態。當狀態發生轉換時, promise.then 綁定的方法就會被調用。(當綁定方法時,若是 promise 對象已經處於 fulfilled 或 rejected 狀態,那麼相應的方法將會被馬上調用, 因此在異步操做的完成狀況和它的綁定方法之間不存在競爭關係。)從Pending轉換爲fulfilled或Rejected以後, 這個promise對象的狀態就不會再發生任何變化。所以 then是隻被調用一次的函數,從而也能說明,then生成的是一個新的promise,而不是原來的那個。

 

瞭解完流程以後,就能夠開始繼續研究源碼了。在PromiseKit當中,最經常使用的當屬then,thenInBackground,catch,finally

 

用了catch之後,在傳遞promise的鏈中,一旦中間任何一環產生了錯誤,都會傳遞到catch去執行Error Handler。

 

4.when

一般咱們有這種需求:

在執行一個A任務以前還有1,2個異步的任務,在所有異步操做完成以前,須要阻塞A任務。代碼可能會寫的像下面這樣子:

 

__block int x = 0;

void (^completionHandler)(id, id) = ^(MKLocalSearchResponse *response, NSError *error){

    if (++x == 2) {

        [self finish];

    }

};

[[[MKLocalSearch alloc] initWithRequest:rq1] startWithCompletionHandler:completionHandler];

[[[MKLocalSearch alloc] initWithRequest:rq2] startWithCompletionHandler:completionHandler];

 

這裏就可使用when來優雅的處理這種狀況:

 

id search1 = [[[MKLocalSearch alloc] initWithRequest:rq1] promise];

id search2 = [[[MKLocalSearch alloc] initWithRequest:rq2] promise];

 

PMKWhen(@[search1, search2]).then(^(NSArray *results){

    //…

}).catch(^{

    // called if either search fails

});

 

在when後面傳入一個數組,裏面是2個promise,只有當這2個promise都執行完,纔會去執行後面的then的操做。這樣就達到了以前所說的需求。

 

這裏when還有2點要說的,when的參數還能夠是字典。

 

id coffeeSearch = [[MKLocalSearch alloc] initWithRequest:rq1];

id beerSearch = [[MKLocalSearch alloc] initWithRequest:rq2];

id input = @{@"coffee": coffeeSearch, @"beer": beerSearch};

 

PMKWhen(input).then(^(NSDictionary *results){

    id coffeeResults = results[@"coffee"];

});

 

這個例子裏面when傳入了一個input字典,處理完成以後依舊能夠生成新的promise傳遞到下一個then中,在then中能夠去到results的字典,得到結果。傳入字典的工做原理放在第四章會解釋。

 

when傳入的參數還能夠是一個可變的屬性:

 

@property id dataSource;

 

- (id)dataSource {

    return dataSource ?: [PMKPromise new:…];

}

 

- (void)viewDidAppear {

    [PMKPromise when:self.dataSource].then(^(id result){

        // cache the result

        self.dataSource = result;

    });

}

 

dataSource若是爲空就新建一個promise,傳入到when中,執行完以後,在then中拿到result,並把result賦值給dataSource,這樣dataSource就有數據了。由此看來,when的使用很是靈活!

 

5.always & finally

 

//oc版

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

[self myPromise].then(^{

    //…

}).finally(^{

    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

})

 

//swift版

UIApplication.sharedApplication().networkActivityIndicatorVisible = true

myPromise().then {

    //…

}.always {

    UIApplication.sharedApplication().networkActivityIndicatorVisible = false

}

 

在咱們執行完then,處理完error以後,還有一些操做,那麼就能夠放到finally和always裏面去執行。

 

四.PromiseKit的源碼解析

 

通過上面對promise的方法的學習,咱們已經能夠了解到,在異步操做咱們能夠經過不斷的返回promise,傳遞給後面的then來造成鏈式調用,因此重點就在then的實現了。在討論then以前,我先說一下promise的狀態和傳遞機制。

 

一個promise可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)。

 

一個promise的狀態只可能從「等待」轉到「完成」態或者「拒絕」態,不能逆向轉換,同時「完成」態和「拒絕」態不能相互轉換。

 

promise必須實現then方法(能夠說,then就是promise的核心),並且then必須返回一個promise,同一個promise的then能夠調用屢次,而且回調的執行順序跟它們被定義時的順序一致

 

then方法接受兩個參數,第一個參數是成功時的回調,在promise由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在promise由「等待」態轉換到「拒絕」態時調用。同時,then能夠接受另外一個promise傳入,也接受一個「類then」的對象或方法,即thenable對象

 

 

總結起來就是上圖,pending狀態的promise對象既可轉換爲帶着一個成功值的 fulfilled 狀態,也可變爲帶着一個 error 信息的 rejected 狀態。當狀態發生轉換時, promise.then 綁定的方法就會被調用。(當綁定方法時,若是 promise 對象已經處於 fulfilled 或 rejected 狀態,那麼相應的方法將會被馬上調用, 因此在異步操做的完成狀況和它的綁定方法之間不存在競爭關係。)從Pending轉換爲fulfilled或Rejected以後, 這個promise對象的狀態就不會再發生任何變化。所以 then是隻被調用一次的函數,從而也能說明,then生成的是一個新的promise,而不是原來的那個。

相關文章
相關標籤/搜索