代码开发如何避免”回调地狱”__消除Callback Hell

原创
2020/08/05 13:44
阅读数 225

假设这样一个简单的场景:登录页用户输入账号密码之后,发起登录请求login,该接口的回调返回了一个token值,后面该用户处于登录状态下所有请求都要用到该token值。接着调用了一个获取用户个人信息的接口personInfo,该接口返回登录用户的详细信息,之后又调用了一个接口appInit,是获取APP进入后要配置的各项参数,比如APP启动页的3s广告的图片地址,APP针对不同的机型做出的不同页面显示等等功能。。。

这样的3个接口,一个接着一个,处理起来不见得有多麻烦,我列一下请求的流程:

//第一个接口
login(account,pwd)(){
token = response['token'];
//第二个接口
personInfo(token)(){
userModel = userModel.fromJson(response);
//第三个接口
appInit(){
print(请求到了第三个接口);
}
}
}

大概这样的一个流程走下来,每个接口请求回调中都有各自的工作需要进行,还要有针对不同errorCode的判断和处理,那么整体一个流程下来后完整的代码少说几十行多的上百行,看起来着实难受。

虽然可以通过封装的方法将3个请求分成3个方法,然后在每个接口的成功处将调用下一个方法的代码写上。但是仍然给人的感觉不是很流畅。不流畅的原因在于其一是各个方法需要不同的参数,回调处要处理的代码和下一个接口要调用的参数耦合到了一起,看起来代码不整洁。其二是各自的流程和目的不同,接到一起造成代码的可读性下降和出错率升高,这是饱受诟病的一点。

那么如何解决这个问题呢,在iOS上和Flutter有各自不同的方案对应,我会分别举例说明


iOS:

解决方法是使用RAC ,使用过RAC的同学都知道这是干什么用的,具体的使用说明不做介绍,有不明白的可以看看文章:https://blog.csdn.net/qq_27633421/article/details/52212484

接下来说说如何处理,我们要用到的就是RAC中的小功能then函数。then从字面上理解就是然后的意思,我先发出1号请求,然后再发出2号请求,两个请求都用RAC的signal管理起来,各自不相干不影响。

先说一下原理:

// then:用于连接两个信号,当第一个信号完成,才会连接then返回的信号
// 注意使用then,之前信号的值会被忽略掉.
// 底层实现:1、先过滤掉之前的信号发出的值。2.使用concat连接then返回的信号
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}] then:^RACSignal *{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
}] subscribeNext:^(id x) {
// 只能接收到第二个信号的值,也就是then返回信号的值
NSLog(@"%@",x);
}];

逻辑是这样的:

信号1 = 请求1
信号2 = 请求2
信号3 = 信号1 then(){
信号2
}
信号3 执行

上图的意思就是我把请求1整体返回一个信号,请求2再返回一个信号,然后用一个新的信号把它俩组合起来,让请求1发出,执行完成后调用then函数,在then函数中调用请求2,接下来还可以如此操作,调用请求3,请求4。。。。

上代码:

#pragma mark 密码登录
-(RACSignal *)signInWithPassWord{
RACSignal *tokenSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSMutableDictionary *param = [NSMutableDictionary dictionary];
param[@"tel"] = _phoneTextF.text;
param[@"password"] = _mmTextF.text;

[[NetworkTools shareNetworkTools] requestWithMethod:post andUrl:@"login/login_pw" andParams:param complete:^(id resp) {
_token = resp[@"token"];
NSLog(@"第一步获取token");
[subscriber sendNext:@“”];
[subscriber sendCompleted];
} fail:^(NSString *msg, NSInteger code) {
[HBHUDTool showMessageCenter:msg];
}];
return nil;
}];
return tokenSignal;
}

这段代码整体请求作为一个信号来执行,请求完成后在内部由RACSubscriber的实例化对象也就是信号的订阅者来发出请求完成的信号,告诉外部那个大信号,我1号请求的任务完成,你可以继续进行接下来的操作了! 它告诉外部大信号的代码就是:

[subscriber sendNext:@“”];
[subscriber sendCompleted];

这两句话。

然后我们看接下来信号2的操作:

#pragma mark 获取用户信息
-(RACSignal *)loadPersonInfo{
RACSignal *personSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSMutableDictionary *param1 = [NSMutableDictionary dictionary];
param1[@"token"] = _token;
[[NetworkTools shareNetworkTools] requestWithMethod:post andUrl:@"login/is_login" andParams:param1 complete:^(id resp) {
UserRecord *record = [UserRecord mj_objectWithKeyValues:resp];
[HBHUDTool showMessageCenter:@"登录成功"];
record.isLogin = YES;
record.token = _token;
[CommonModel shareInstance].loginData = [NSKeyedArchiver archivedDataWithRootObject:record];
NSLog(@"第二步获取用户信息");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if ([[UIApplication sharedApplication].keyWindow.rootViewController isKindOfClass:[NFUserLoginViewController class]]) {
HQMainTabbarVC *tabVC = [[HQMainTabbarVC alloc]init];
[UIApplication sharedApplication].keyWindow.rootViewController =tabVC;
}else{
[self dismissViewControllerAnimated:YES completion:^{
}];
}
});
[subscriber sendNext:nil];
[subscriber sendCompleted];
} fail:^(NSString *msg, NSInteger code) {
[HBHUDTool showMessageCenter:msg];
}];
return nil;
}];
return personSignal;
}

这是信号2的操作,具体执行和信号1差不多,内部逻辑稍有不同,不过目的都是一样的为了完成操作后发给外部大信号一个“通知”

外部调用:

[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
RACSignal *then = [[self signInWithPassWord] then:^RACSignal *{
return [self loadPersonInfo];
}];
[then subscribeNext:^(id x) {
}];
}];

也可以这样写,其实是一样的:

[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
[[[self signInWithPassWord] then:^RACSignal *{
return [self loadPersonInfo];
}] subscribeNext:^(id x) {
NSLog(@"完成");
}];
}];

来看一遍执行的效果:

的确是按照我们预想的顺序执行,并且方法中只含有各自的处理逻辑,没有做额外的工作,binggo,就是我们要的结果!


Flutter

flutter中处理起来要比在iOS中简单的多,得益于flutter中的三个关键字,Future,async,await

Future:未来,就是用来处理异步操作的,一个函数返回了future后就是一个异步函数,它的执行结果在未来某个时间段才会有。future是一个对象,对应了一个处理结果,要么成功要么失败。

async 异步,通常网络请求方法名后面要跟随这个关键字,表示方法异步执行,未来某个时间段才有返回值。

await 等待,该关键字必须出现在async函数中,否则报错

接下来说明说一下在flutter中处理回调地狱的思路,还是用上面的3个接口举例:

Future loginAction(){
Map param = Map();
param['tel'] = userName;
param['password'] = userPassWord;

String token;
final complete = Completer();
final future = complete.future;
EasyLoading.show(status: '登录中');
HttpUtil.instance.postData('http://xxxx.com/login_pw', param, RequestLisener(onSucessLisener: (BaseResponse rep) async{
EasyLoading.dismiss();
EasyLoading.show(status: '登录中');
token = await rep.data['token'];
complete.complete(token);
}, onFailLisener: (BaseResponse rep){
Fluttertoast.showToast(msg: rep.msg);
EasyLoading.dismiss();
}));
return future;
}

上面的代码中,方法名:Future loginAction() 定义了一个返回future对象的函数,意思这个函数未来会有返回值,什么时候不确定。

然后定义了两个final的属性complete和future,complete是Completer类型的,Completer类似于future,但是用法不一样,我们知道在future方法中会有相应的回调,通过那些Future构造函数生成的Future对象其实控制权不在你这里。它什么时候执行完毕只能等系统调度了。你只能被动的等待Future执行完毕然后调用你设置的回调。如果你想手动控制某个Future怎么办呢?请使用Completer。Completer内部包含了一个future,使用completer的话控制权完全在你自己的代码手里

关于completer和future的异同有篇文章介绍的很仔细:https://juejin.im/post/5c4875f86fb9a049ff4e78cf

我们接着说这个异步函数,在有返回值的时候,我们用completer来手动控制回调,执行complete的动作并传入参数后,方法结束,返回future对象。这是第一个请求

第二个请求:

  Future loadPersonInfo(){
final complete = Completer();
final future = complete.future;
HttpUtil.instance.postData(’http://xxxx.com/person_info‘, null, RequestLisener(onSucessLisener: (BaseResponse rep) async{
Fluttertoast.showToast(msg: '登录成功',gravity: ToastGravity.CENTER);
Future.delayed(Duration(seconds: 1),(){
Navigator.pop(context);
});
EasyLoading.dismiss();
print('登录成功');
complete.complete();
},onFailLisener: (BaseResponse rep){
Fluttertoast.showToast(msg: rep.msg,gravity: ToastGravity.CENTER);
EasyLoading.dismiss();
}));
return future;
}

写法同第一个请求基本一样。你可以在里面写上自己的逻辑。

然后是调用了:

child: FlatButton(
onPressed: (){
print('点击登录');

loginAction().then((token){
print('token:'+token);
return token;
}).then((value){
return loadPersonInfo();
}).then((value) {
print('登录后刷新各页面状态');
});
},
child: Text('登录',style: TextStyle(fontSize: W(15),color: Colors.white),),
),

由于我们每个网络请求的方法返回的都是future对象,所以可以在方法执行完成后使用then函数,在then函数中就是我们在方法内部通过complete方法传入的参数了。

这样一来是不是网络请求的嵌套调用瞬间清爽多咯~


本文分享自微信公众号 - iOS程序开发交流(zhangqq16781)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部