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

来源:互联网 发布:淘宝秒杀抢购软件 编辑:程序博客网 时间:2024/04/29 05:20

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


前言


最近看了一些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写出的代码就是这样:


[selflogin].then(^{

 

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

    return[APIfetchData];

 

}).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.tableViewreloadData];

 

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

 

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

    [[[UIAlertViewalloc] 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 installcocoapods &&podsetup


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


//移除原有的墙外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


2.找到项目的路径,进入项目文件夹下面,执行:


$touchPodfile &&open -ePodfile


此时会打开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


$podinstall


安装完成之后,退出终端,打开新生成的.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];

    }

 

}


上面的写法也不是错误的,就是它在调用函数中保存了一个属性,在调用alertView会使用到这个属性。其实这个中间属性是不需要存储的。接下来我们就用then来去掉这个中间变量。


-(void)showUndoRedoAlert:(UndoRedoState*)state

{

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

    [alertpromise].then(^(NSNumber*dismissedButtonIndex){

        [statedo];

    });

}


这时就有人问了,为啥能调用 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;


对应的实现是这样的


-(PMKPromise*)promise{

    PMKAlertViewDelegater*d = [PMKAlertViewDelegaternew];

    PMKRetain(d);

    self.delegate = d;

    [selfshow];

    return[PMKPromise new:^(idfulfiller,idrejecter){

        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_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);

        });

    });

}


使用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 ^(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);

}


看了源码就知道上述是正确的。


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;

                }

            }];

        }

    }

}];


我们可以用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{errorin

    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];


这里就可以使用when来优雅的处理这种情况:


idsearch1 = [[[MKLocalSearchalloc] initWithRequest:rq1] promise];

idsearch2 = [[[MKLocalSearchalloc] 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"];

});


这个例子里面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;

    });

}


dataSource如果为空就新建一个promise,传入到when中,执行完之后,在then中拿到result,并把result赋值给dataSource,这样dataSource就有数据了。由此看来,when的使用非常灵活!


5.always & finally


//oc版

[UIApplicationsharedApplication].networkActivityIndicatorVisible = YES;

[selfmyPromise].then(^{

    //…

}).finally(^{

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

0 0
原创粉丝点击