关于回调地狱(Callback hell ),你不知道吧。 尤其是对于前端朋友,当然前端朋友通过Promise等各种方式避免回调地狱。 但是,我在后端的朋友中听说了很多关于回调地狱的事情,尤其是在反应编程框架(如RxJava和Reactor )兴起之后,却不多见。
为了更好地知道回调地狱Callback hell的问题在哪里,我们需要先学习如何写回调地狱。 在那之前,你必须知道什么是回调函数。
本文包括以下内容。
什么是回调
回调的优点
什么是回调地狱
为什么会发生回调地狱
回调和Future的区别是什么
如何解决回调地狱
今天我们从一开始就谈谈什么是回调函数。
什么是回调函数?
百度百科上这样说:
回调函数是从函数指针调用的函数。 如果将函数的指针(地址)作为参数传递给另一个函数,当用于调用该指针指向的函数时,就会说这是回调函数。 回调函数不是直接从函数的实现方调用的,而是在发生特定事件或条件时从另一方调用的,用于响应该事件或条件。 回调是由将方法作为第一个参数的其他方法调用的方法。 在许多情况下,回调是在发生特定事件时调用的方法。
是什么? 很难理解吗? 确实很难理解,这个说明里有指针云,对java用户真的很不友好。
作为参考,举个例子吧。 也欢迎批评和指摘。
回叫:呼叫方呼叫被叫方后,被叫方将结果反馈给呼叫方。 ) a调用b,b完成后,将结果反馈给a )举例来说,上司给员工安排工作,员工完成它。 员工完成工作后,向上司反馈工作结果。 这个过程称为回调。
这样应该很容易理解了吧。 Talk is cheap,Show me the code! 那么,用这个写简单的例子吧。
回调示例
呼叫接口
首先,写下以下Callback接口: 接口只包含一个用于Callback操作的方法。
//*
* @author振动的月饼
*/
公共接口调用{
//*
*具体实施
* @param t
*/
公共void callback (TT;
() ) ) ) )。
Boss类
因为上司是被反馈的对象,所以对于需要实现Callback这个接口并重载Callback方法的上司具体做什么,当然是做大生意,有makeBigDeals方法他需要员工。 我们重建方法向他添加员工Worker,稍后实现Worker类。
publicclassbossimplementscallback {
私有工作器工作器;
public boss (工作器工作器) {
this.worker=worker;
() ) ) ) )。
@Override
公共void callback (strings ) {
() ) ) ) )。
publicvoidmakebigdeals (finalstringsomedetail )。
worker.work(somedetail;
() ) ) ) )。
() ) ) ) )。
工作器类
员工类很简单。 出入一项工作,完成就行了。 把结果还给我就行了。 但是,如何完成回调呢?
公共类工作器{
公共字符串工作(字符串工作)。
返回'结果';
() ) ) ) )。
() ) ) ) )。
虽然我们在这种思路上很容易被认为非常符合思维逻辑,但在回调中,我们需要做一些改变。
调用代码
对员工来说,有两点是必须知道的。 谁是上司,需要做什么? 因此,输入上司和工作内容这两个参数。 具体内容分两个阶段,先完成任务,然后向上司报告。
公共类工作器{
公共语音工作(callback boss,String someWork )。
String result=someWork 'is done!' ; //作出具体处理
BOSS.callback(result ); //把结果反馈给上司
() ) ) ) )。
() ) ) ) )。
接下来,完成Boss类。 callback方法接收发送的结果并处理结果。 这里只需要打印。 在makeBigDeals方法中,上司分配工作,员工完成。 如果完成进程是异步的,则为异步调用;如果完成进程是同步的,则为同步回调。 这里采用的是异步方式。
在新线程上,运行Worker.Work(Boss.thi )
s, someDetail),其中Boss.this即为当前对象,在这里,我们正式完成了回调。public class Boss implements Callback {
……
@Override
public void callback(String result) { // 参数为worker输出的结果
logger.info("Boss got: {}", result) // 接到完成的结果,并做处理,在这里我们仅打印出来
}
public void makeBigDeals(final String someDetail) {
logger.info("分配工作");
new Thread(() -> worker.work(Boss.this, someDetail)); // 异步完成任务
logger.info("分配完成");
logger.info("老板下班。。");
}
}
回调结果
Show me the result! 好,跑一下代码试一下。
Worker worker = new Worker();
Boss boss = new Boss(worker); // 给老板指派员工
boss.makeBigDeals("coding"); // 老板有一个代码要写
结果如下。在结果中可以看到,老板在分配完工作后就下班了,在下班后,另一个线程通知老板收到反馈"coding is done"。至此,我们完成了异步回调整个过程。
INFO 2019 九月 20 11:30:54,780 [main] - 分配工作
INFO 2019 九月 20 11:30:54,784 [main] - 分配完成
INFO 2019 九月 20 11:30:54,784 [main] - 老板下班。。
INFO 2019 九月 20 11:30:54,787 [Thread-0] - Boss got: coding is done!
我将代码示例传至Github,供大家参考。 callback代码示例
回调的优势
解耦,回调将子过程从主过程中解耦。 对于相同的输入,可能对其有不同的处理方式。在回调函数,我们完成主流程(例如上面的Boss类),对于过程中的子流程(例如上面的Worker类)从主流程中分离出来。对于主流程,我们只关心子过程的输入和输出,输入在上面的例子中即为Worker.work中的参数,而子过程的输出则是主过程的callback方法的参数。
异步回调不会阻塞主线程。上面的例子清晰可以看到,员工没有完成工作之前老板就已经下班,当工作完成后,会通过另一个线程通知老板。老板在这个过程无需等待子过程。
回调地狱
总体设计
我们将上述功能扩展,老板先将工作交给产品经理进行设计;设计完成后,交给程序员完成编码。流程示意如图。
将任务交给产品经理
首先,写一个Callback,内部new一个产品经理的的Worker,在makeBigDeal方法实现主任务,将任务交给产品经理;在重载的callback方法中,获取产品经理的输出。
new Callback() {
private Worker productManager = new Worker();
@Override
public void callback(String s) {
System.out.println("产品经理 output: " + s); // 获取产品经理的输出
}
public void makeBigDeals(String bigDeal) {
System.out.println("Boss将任务交给产品");
new Thread(() -> {
this.productManager.work(this, bigDeal); // 异步调用产品经理处理过程
}).start();
}
}.makeBigDeals("design");
再将产品经理输出交给开发
在拿到产品经理的输出之后,再将输出交给开发。于是我们在再次实现一个Callback接口。同样的,在Callback中,new一个开发的Worker,在coding方法中,调用Worker进行开发;在重载的callback方法中,获取开发处理后的结果。
@Override
public void callback(String s) {
System.out.println("产品经理 output: " + s); // 产品经理的输出
String midResult = s + " coding";
System.out.println("产品经理设计完成,再将任务交给开发");
new Callback() {
private Worker coder = new Worker();
@Override
public void callback(String s) {
System.out.println("result: " + s); // 获取开发后的结果
}
public void coding(String coding) {
new Thread(() -> coder.work(this, coding)).start(); // 调用开发的Worker进行开发
}
}.coding(midResult); // 将产品经理的输出交给开发
}
完整的实现
new Callback() {
private Worker productManager = new Worker();
@Override
public void apply(String s) {
System.out.println("产品经理 output: " + s);
String midResult = s + " coding";
System.out.println("产品经理设计完成,再将任务交给开发");
new Callback() {
private Worker coder = new Worker();
@Override
public void apply(String s) {
System.out.println("result: " + s);
}
public void coding(String coding) {
new Thread(() -> coder.work(this, coding)).start();
}
}.coding(midResult);
}
public void makeBigDeals(String bigDeal) {
System.out.println("Boss将任务交给产品");
new Thread(() -> this.productManager.work(this, bigDeal)).start();
}
}.makeBigDeals("design");
好了,一个简单的回调地狱完成了。Show me the result!
Boss将任务交给产品
产品经理 output: design is done!
产品经理设计完成,再将任务交给开发
result: design is done! coding is done!
回调地狱带来了什么?
到底什么是回调地狱?简单的说,回调地狱就是Callback里面又套了一个Callback,但是如果嵌套层数过多,仿佛掉入地狱,于是有了回调地狱的说法。
优势: 回调地狱给我们带来什么?事实上,回调的代码如同管道一样,接收输入,并将处理后的内容输出至下一步。而回调地狱,则是多个管道连接,形成的一个流程,而各个子流程(管道)相互独立。前端的朋友可能会更熟悉一些,例如Promise.then().then().then(),则是多个处理管道形成的流程。
劣势: 回调的方法虽然将子过程解耦,但是回调代码的可读性降低、复杂性大大增加。
Callback Hell示例:Callback Hell
和Future对比
在上面,我们提到异步回调不会阻塞主线程,那么使用Future也不会阻塞,和异步回调的差别在哪?
我们写一个使用Future来异步调用的示例:
logger.info("分配工作...");
CompletableFuture future = CompletableFuture.supplyAsync(() -> worker.work(someDetail));
logger.info("分配完工作。");
logger.info("老板下班回家了。。。");
logger.info("boss got the feedback from worker: {}", future.get());
在上面的代码,我们可以看到,虽然Worker工作是异步的,但是老板获取工作的结果(future.get())的时候却需要等待,而这个等待的过程是阻塞的。这是回调和Future一个显著的区别。
如何解决
如何解决回调地狱的问题,最常用的就是反应式编程RxJava和Reactor,还有Kotlin的Coroutine协程,OpenJDK搞的Project Loom。其中各有优势,按下不表。
总结
总结一下:
什么是回调。回调是调用方在调用被调方后,被调方还将结果反馈给调用方。(A调用B,B完成后,将结果反馈给A)
回调的优势。1)子过程和主过程解耦。2)异步调用并且不会阻塞主线程。
回调地狱是什么。回调地狱是回调函数多层嵌套,多到看不清=。=
为什么会出现回调地狱。每一个回调像一个管道,接受输出,处理后将结果输出到下一管道。各个管道处理过程独立,多个管道组成整个处理过程。
回调和Future有什么区别。1)两者机制不同;2)Future在等待结果时会阻塞,而回调不会阻塞。
如何解决回调地狱。最常见的则是反应式编程RxJava和Reactor。