iOS如何优雅的处理“回调地狱Callback hell”(一)——使用PromiseKit

640?wx_fmt=jpeg

黑客技术 点击右侧关注,了解黑客的世界! 640?wx_fmt=jpeg

640?wx_fmt=jpeg

Linux编程 点击右侧关注,免费入门到精通! 640?wx_fmt=jpeg

作者丨一缕殇流化隐半边冰霜
https://juejin.im/post/5779b347165abd0054b92a78

前言


最近看了一些Swift关于封装异步操作过程的文章,比如RxSwift,RAC等等,因为回调地狱我自己也写过,很有感触,于是就翻出了Promise来研究学习一下。现将自己的一些收获分享一下,有错误欢迎大家多多指教。


目录


  • 1.PromiseKit简介

  • 2.PromiseKit安装和使用

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

  • 4.PromiseKit的源码解析

  • 5.使用PromiseKit优雅的处理回调地狱


一.PromiseKit简介


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


A promise is an object that wraps an asynchronous task


Promise也是一个包装着异步操作的一个对象。使用PromiseKit,能够编写出整洁,有序的代码,逻辑简单的,将Promise作为参数,模块化的从一个异步任务到下一个异步任务中去。用PromiseKit写出的代码就是这样:


 
 
 
 
[selflogin].then(^{// our login method wrapped an async task in a promisereturn[APIfetchData];}).then(^(NSArray*fetchedData){// our API class wraps our API and returns promises// fetchedData returned a promise that resolves with an array of dataself.datasource = fetchedData;[self.tableViewreloadData];}).catch(^(NSError*error){// any errors in any of the above promises land here[[[UIAlertViewalloc] init…] show];});
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(Swift),一个是AnyPromise(Objective-C),2者的区别就在2种语言的特性上,Promise是定义精确严格的,AnyPromise是定义宽松,灵活,动态的。


今天就让我们动起手来,用PromiseKit来优雅的处理掉Callback hell吧。


二.PromiseKit安装和使用


1.下载安装CocoaPods


在墙外的安装步骤:


在Terminal里面输入


 
 
 
 
sudo gem installcocoapods &&podsetup
cocoapods &&
pod
setup


大多数在墙内的同学应该看如下步骤了:


 
 
//移除原有的墙外Ruby 默认源$gemsources --removehttps://rubygems.org///添加现有的墙内的淘宝源$gemsources -ahttps://ruby.taobao.org///验证新源是否替换成功$gemsources -l//下载安装cocoapods// OS 10.11之前$sudo gem installcocoapods//mark:OS 升级 OS X EL Capitan 后命令应该为:$sudo geminstall -n /usr/local/bincocoapods//设置cocoapods$podsetup


$
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.找到项目的路径,进入项目文件夹下面,执行:


 
 
$touchPodfile &&open -ePodfile
Podfile &&
open -e
Podfile


此时会打开TextEdit,然后输入一下命令:


 
 
platform:ios,‘7.0’target'PromisekitDemo'do  //由于最新版cocoapods的要求,所以必须加入这句话    pod'PromiseKit'end
7.0





target
'PromisekitDemo'
do  //由于最新版cocoapods的要求,所以必须加入这句话


    pod
'PromiseKit'


end


 
 
Tips:感谢qinfensky大神提醒,其实这里也可以用init命令Podfile是CocoaPods的特殊文件,在其中可以列入在项目中想要使用的开源库,若想创建Podfile,有2种方法:1.在项目目录中创建空文本文件,命名为Podfile2.或者可以再项目目录中运行“$ pod init “,来创建功能性文件(终端中输入cd 文件夹地址,然后再输入 pod init)两种方法都可以创建Podfile,使用你最喜欢使用的方法

Podfile是CocoaPods的特殊文件,在其中可以列入在项目中想要使用的开源库,若想创建Podfile,有2种方法:


1.在项目目录中创建空文本文件,命名为Podfile


2.或者可以再项目目录中运行“$ pod init “,来创建功能性文件(终端中输入cd 文件夹地址,然后再输入 pod init)


两种方法都可以创建Podfile,使用你最喜欢使用的方法


3.安装PromiseKit


 
 
$podinstall
install


安装完成之后,退出终端,打开新生成的.xcworkspace文件即可


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


1.then

经常我们会写出这样的代码:


 
 
-(void)showUndoRedoAlert:(UndoRedoState*)state{UIAlertView*alert = [[UIAlertViewalloc] initWithTitle:……];alert.delegate = self;self.state = state;[alertshow];}-(void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{    if(buttonIndex == 1){        [self.statedo];    }}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 = [[UIAlertViewalloc] initWithTitle:……];    [alertpromise].then(^(NSNumber*dismissedButtonIndex){        [statedo];    });}
*)state


{


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


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


        [state
do];


    });


}


这时就有人问了,为啥能调用 alert promise 这个方法?后面点语法跟着then是什么?我来解释一下,原因其实只要打开Promise源码就一清二楚了。在pormise源码中


 
 
@interfaceUIAlertView(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;
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 = [PMKAlertViewDelegaternew];    PMKRetain(d);    self.delegate = d;    [selfshow];    return[PMKPromise new:^(idfulfiller,idrejecter){        d->fulfiller = fulfiller;    }];}
*)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放在源码分析里面去讲讲。


2.dispatch_promise


项目中我们经常会异步的下载图片


 
 
typedefvoid(^onImageReady)(UIImage*image);+(void)getImageWithURL:(NSURL*)url onCallback:(onImageReady)callback{    dispatch_queue_tqueue = 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);        });    });}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    });

        return
[NSData
 dataWithContentsOfURL:url];    


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


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


    }).then(^{


        //
 add code to happen next here


    });


我们看看源码,看看调用的异步过程对不对


 
 
-(PMKPromise *(^)(id))then{    return ^(idblock){        returnself.thenOn(dispatch_get_main_queue(),block);    };}PMKPromise*dispatch_promise(idblock){    returndispatch_promise_on(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),block);}
 *(^)(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{            idrq = [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;                }            }];        }    }}];
(^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];})


[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{errorin    UIAlertView(…).show()}


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任务。代码可能会写的像下面这样子:


 
 
__blockintx = 0;void(^completionHandler)(id,id) = ^(MKLocalSearchResponse*response,NSError*error){    if(++x == 2){        [selffinish];    }};[[[MKLocalSearchalloc] initWithRequest:rq1] startWithCompletionHandler:completionHandler];[[[MKLocalSearchalloc] initWithRequest:rq2] startWithCompletionHandler:completionHandler];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来优雅的处理这种情况:


 
 
idsearch1 = [[[MKLocalSearchalloc] initWithRequest:rq1] promise];idsearch2 = [[[MKLocalSearchalloc] initWithRequest:rq2] promise];PMKWhen(@[search1,search2]).then(^(NSArray*results){    //…}).catch(^{    // called if either search fails});
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的参数还可以是字典。


 
 
idcoffeeSearch = [[MKLocalSearchalloc] initWithRequest:rq1];idbeerSearch = [[MKLocalSearchalloc] initWithRequest:rq2];idinput = @{@"coffee": coffeeSearch,@"beer": beerSearch};PMKWhen(input).then(^(NSDictionary*results){    idcoffeeResults = results[@"coffee"];});
 = [[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传入的参数还可以是一个可变的属性:


 
 
@propertyiddataSource;-(id)dataSource{    returndataSource?:[PMKPromise new:…];}-(void)viewDidAppear{    [PMKPromise when:self.dataSource].then(^(idresult){        // cache the result        self.dataSource = result;    });}
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版[UIApplicationsharedApplication].networkActivityIndicatorVisible = YES;[selfmyPromise].then(^{    //…}).finally(^{    [UIApplicationsharedApplication].networkActivityIndicatorVisible = NO;})


[UIApplication
sharedApplication].networkActivityIndicatorVisible
 = YES;


[self
myPromise].then(^{


    //…


}).finally(^{


    [UIApplication
sharedApplication].networkActivityIndicatorVisible
 = NO;


})


 
 
//swift版UIApplication.sharedApplication().networkActivityIndicatorVisible = truemyPromise().then{    //…}.always{    UIApplication.sharedApplication().networkActivityIndicatorVisible = false}


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,而不是原来的那个。


 推荐↓↓↓ 

640?wx_fmt=jpeg

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

640?wx_fmt=png万水千山总是情,点个 “ 在看” 行不行
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值