前言

最近看了一些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

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

iOS 如何优雅的处理“回调地狱Callback hell”(一) (上)的更多相关文章

  1. iOS 如何优雅的处理“回调地狱Callback hell”(一) (下)

    了解完流程之后,就可以开始继续研究源码了.在PromiseKit当中,最常用的当属then,thenInBackground,catch,finally - (PMKPromise *(^)(id)) ...

  2. js中的回调地狱 Callback to Hell

        本文重点:解决方式:1.promise  2. 拆解 function:将各步拆解为单个的 function  3. 通过 Generator 函数暂停执行的效果方式 4. 通过ES8的异步函 ...

  3. callback hell (回调地狱)

    callback hell (回调地狱) callback(回调) 如何修复 callback hell callback 回调只是存放一些即将要处理的代码. 回调的执行顺序不是从上到下的,而是根据事 ...

  4. js中promise解决callback回调地狱以及使用async+await异步处理的方法

    1.callback回调地狱 function ajax(fn) { setTimeout(()=> { console.log('你好') fn() }, 1000) } ajax(() =& ...

  5. 深入了解Promise对象,写出优雅的回调代码,告别回调地狱

    深入浅出了解Promise 引言 正文 一.Promise简介 二.Promise的三种状态 三.函数then( ) 四.函数catch( ) 五.函数finally( ) 六.函数all( ) 七. ...

  6. 避免Node.js中回调地狱

    为了解决这个阻塞问题,JavaScript严重依赖于回调,这是在长时间运行的进程(IO,定时器等)完成后运行的函数,因此允许代码执行经过长时间运行的任务. downloadFile('example. ...

  7. ES6(promise)_解决回调地狱初体验

    一.前言 通过这个例子对promise解决回调地狱问题有一个初步理解. 二.主要内容 1.回调地狱:如下图所示,一个回调函数里面嵌套一个回调函数,这样的代码可读性较低也比较恶心 2.下面用一个简单的例 ...

  8. JavaScript 中回调地狱的今生前世

    1. 讲个笑话 JavaScript 是一门编程语言 2. 异步编程 JavaScript 由于某种原因是被设计为单线程的,同时由于 JavaScript 在设计之初是用于浏览器的 GUI 编程,这也 ...

  9. 使用ES6的Promise完美解决回调地狱

    相信经常使用ajax的前端小伙伴,都会遇到这样的困境:一个接口的参数会需要使用另一个接口获取. 年轻的前端可能会用同步去解决(笑~),因为我也这么干过,但是极度影响性能和用户体验. 正常的前端会把接口 ...

随机推荐

  1. ThinkPHP框架下,给jq动态添加的标签添加点击事件移除标签

    jq移除标签主要就是$("#要移除的id").remove();不再赘述,这里要提醒的是jq中动态添加标签后怎样添加点击事件.一般的jq添加点击事件是用这种方法$("#i ...

  2. __call方法简介

    作用:当程序试图调用不存在或不可见的成员方法时,PHP会先调用__call方法来储方法名及参数. __call方法包含两个参数:即方法名和方法参数.其中,方法参数是以数组形式存在的.

  3. PHP面向对象(OOP):抽象方法和抽象类(abstract)

    在OOP语言中,一个类可以有一个或多个子类,而每个类都有至少一个公有方法做为外部代码访问其的接口.而抽象方法就是为了方便继承而引入的,我们先来看一下抽象类和抽象方法的定义再说明它的用途. 什么是抽象方 ...

  4. uboot使用tftp下载时出现“checksum bad”问题原因分析

    一.问题 二.原因分析 你的虚拟机是不是这样设置的呢? 如果是的话,请看下边的解释: 使用NAT模式,就是让虚拟系统借助NAT(网络地址转换)功能,通过宿主机器所在的网络来访问公网.也就是说,使用NA ...

  5. ssh远程登录Ubuntu报错:Permission denied, please try again.

    ssh到server上的时候密码是对的但是报如下信息:# ssh 172.16.81.221root@172.16.81.221's password:Permission denied, pleas ...

  6. Squares<哈希>

    Description A square is a 4-sided polygon whose sides have equal length and adjacent sides form 90-d ...

  7. PHP+Mysql-表单数据插入数据库及数据提取完整过程

    网站在进行新用户注册时,都会将用户的注册信息存入数据库中,需要的时候再进行提取.今天写了一个简单的实例. 主要完成以下几点功能: (1)用户进行注册,实现密码重复确认,验证码校对功能. (2)注册成功 ...

  8. 【HDOJ】2645 find the nearest station

    裸BFS. /* 2645 */ #include <iostream> #include <queue> #include <cstdio> #include & ...

  9. Linux后台进程管理的一些命令小结

    Linux后台进程管理的一些命令:fg.bg.jobs.&.ctrl + z命令,供大家学习参考   一. &加在一个命令的最后,可以把这个命令放到后台执行 ,如gftp &, ...

  10. (转载)mysql查询今天、昨天、7天、近30天、本月、上一月数据

    (转载)http://blog.163.com/dreamman_yx/blog/static/26526894201053115622827/ 查询 今天 select * from 表名 wher ...