其他
10张图让你彻底理解回调函数
不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数?
这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器。
一切要从这样的需求说起
调用make_youtiao() 等待该函数执行完成 该函数执行完后继续后续流程
保存当前被执行函数的上下文 开始执行make_youtiao()这个函数 make_youtiao()执行完后,控制转回到调用函数中
现实并不容易
为什么我们需要回调callback
make_youtiao(10000);
sell();
可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。make_youtiao(10000, sell);
看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外还可以指定制作好后该干什么,第二个被make_youtiao这个函数调用的函数就叫回调,callback。现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样:void make_youtiao(int num, func call_back) {
// 制作油条
call_back(); //执行回调
}
这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。有的同学可能还是有疑问,为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢?不要忘了明日油条这个App是由A小组和B小组同时开发的,A小组在编写make_youtiao时怎么知道B小组要怎么用这个模块,假设A小组真的自己定义sell函数就会这样写:void make_youtiao(int num) {
real_make_youtiao(num);
sell(); //执行回调
}
同时A小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么A小组该怎么写呢?void make_youtiao(int num) {
real_make_youtiao(num);
if (Team_B) {
sell(); // 执行回调
} else if (Team_D) {
store(); // 放到仓库
}
}
故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗?这样的话A小组的同学只需要维护make_youtiao这个函数就能做到工作量饱满了,显然这是一种非常糟糕的设计。所以你会看到,制作完油条后接下来该做什么不是实现make_youtiao的A小组该关心的事情,很明显只有调用make_youtiao这个函数的使用方才知道。因此make_youtiao的A小组完全可以通过回调函数将接下来该干什么交给调用方实现,A小组的同学只需要针对回调函数这一抽象概念进行编程就好了,这样调用方在制作完油条后不管是卖掉、放到库存还是自己吃掉等等想做什么都可以,A小组的make_youtiao函数根本不用做任何改动,因为A小组是针对回调函数这一抽象概念来编程的。以上就是回调函数的作用,当然这也是针对抽象而不是具体实现进行编程这一思想的威力所在。面向对象中的多态本质上就是让你用来针对抽象而不是针对实现来编程的。异步回调
make_youtiao(10000, sell);
// make_youtiao函数返回前什么都做不了
void make_youtiao(int num, func call_back) {
real_make_youtiao(num);
call_back(); //执行回调
}
看到了吧,由于我们要制作10000个油条,make_youtiao函数执行完需要10分钟,也就是说即便我们使用了回调,调用方完全不需要关心制作完油条后的后续流程,但是调用方依然会被阻塞10分钟,这就是同步调用的问题所在。如果你真的理解了上一节的话应该能想到一种更好的方法了。没错,那就是异步调用。反正制作完油条后的后续流程并不是调用方该关心的,也就是说调用方并不关心make_youtiao这一函数的返回值,那么一种更好的方式是:把制作油条的这一任务放到另一个线程(进程)、甚至另一台机器上。如果用线程实现的话,那么make_youtiao就是这样实现了:void make_youtiao(int num, func call_back) {
// 在新的线程中执行处理逻辑
create_thread(real_make_youtiao,
num,
call_back);
}
make_youtiao(10000, sell);
// 立刻返回
// 执行后续流程
这时调用方的后续流程可以和制作油条同时进行,这就是函数的异步调用,当然这也是异步的高效之处。新的编程思维模式
调用某个函数,获取结果 处理获取到的结果
res = request();
handle(res);
这就是函数的同步调用,只有request()函数返回拿到结果后,才能调用handle函数进行处理,request函数返回前我们必须等待,这就是同步调用,其控制流是这样的:request(handle);
我们根本就不关心request什么时候真正的获取的结果,这是request该关心的事情,我们只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真的获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。这就是异步调用,其控制流是这样的:第一部分是我们来处理的,也就是调用request之前的部分 第二部分不是我们处理的,而是在其它线程、进程、甚至另一个机器上处理的。
正式定义
这就是回调函数的定义了。在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。
回调函数就是一个函数,和其它函数没有任何区别。注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示:
回调的类型
同步回调这种回调就是通常所说的同步回调synchronous callbacks、也有的将其称为阻塞式回调blocking callbacks,或者什么修饰都没有,就是回调,callback,这是我们最为熟悉的回调方式。当我们调用某个函数A并以参数的形式传入回调函数后,在A返回之前回调函数会被执行,也就是说我们的主程序会等待回调函数执行完成,这就是所谓的同步回调。
异步回调不同于同步回调, 当我们调用某个函数A并以参数的形式传入回调函数后,A函数会立刻返回,也就是说函数A并不会阻塞我们的主程序,一段时间后回调函数开始被执行,此时我们的主程序可能在忙其它任务,回调函数的执行和我们主程序的运行同时进行。既然我们的主程序和回调函数的执行可以同时发生,因此一般情况下,主程序和回调函数的执行位于不同的线程或者进程中。
回调对应的编程思维模式
常规模式:调用完S服务后后我去执行X任务, 回调模式:调用完S服务后你接着再去执行X任务,
为什么异步回调越来越重要
回调地狱,callback hell
a = GetServiceA();
b = GetServiceB(a);
c = GetServiceC(b);
d = GetServiceD(c);
代码很清晰,很容易理解有没有。我们知道异步回调的方式会更加高效,那么使用异步回调的方式来写将会是什么样的呢?GetServiceA(function(a){
GetServiceB(a, function(b){
GetServiceC(b, function(c){
GetServiceD(c, function(d) {
....
});
});
});
});
我想不需要再强调什么了吧,你觉得这两种写法哪个更容易理解,代码更容易维护呢?博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,一个不得不去重读一边代码;另一个在一旁骂自己为什么当初选择维护这个项目。异步回调代码稍不留意就会跌到回调陷阱中,那么有没有一种更好的办法既能结合异步回调的高效又能结合同步编码的简单易读呢?幸运的是,答案是肯定的,我们会在后续文章中详细讲解这一技术。总结
往期精选
看完这篇还不懂线程与线程池你来打我(内含20张图)读取文件时,程序经历了什么?终于明白了,一文彻底理解I/O多路复用从小白到高手,你需要理解同步与异步(内含10张图)