乐趣区

关于后端:10张图让你彻底理解回调函数

不知你是不是也有这样的纳闷,咱们为什么须要回调函数这个概念呢?间接调用函数不就能够了?回调函数到底有什么作用?程序员到底该如何了解回调函数?

这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器

所有要从这样的需要说起

假如你们公司要开发下一代国民 App“明日油条”,一款主打解决国民早餐问题的 App,为了放慢开发进度,这款利用由 A 小组和 B 小组协同开发。

其中有一个外围模块由 A 小组开发而后供 B 小组调用,这个外围模块被封装成了一个函数,这个函数就叫 make_youtiao()。

如果 make_youtiao()这个函数执行的很快并能够立刻返回,那么 B 小组的同学只须要:

  1. 调用 make_youtiao()
  2. 期待该函数执行实现
  3. 该函数执行完后持续后续流程

从程序执行的角度看这个过程是这样的:

  1. 保留以后被执行函数的上下文
  2. 开始执行 make_youtiao()这个函数
  3. make_youtiao()执行完后,管制转回到调用函数中

如果世界上所有的函数都像 make_youtiao()这么简略,那么程序员大概率就要就业了,还好程序的世界是简单的,这样程序员才有了存在的价值。

现实情况并不容易

事实中 make_youtiao()这个函数须要解决的数据十分宏大,假如有 10000 个,那么 make_youtiao(10000)不会立即返回,而是可能须要 10 分钟才执行实现并返回。

这时你该怎么办呢?想一想这个问题。

可能有的同学就像把头埋在沙子里的鸵鸟一样:和方才一样间接调用不能够吗,这样多简略。

是的,这样做没有问题,但就像爱因斯坦说的那样“所有都应该尽可能简略,然而不能过于简略”。

想一想间接调用会有什么问题?

显然间接调用的话,那么调用线程会被阻塞暂停,在期待 10 分钟后能力持续运行。在这 10 分钟内该线程不会被操作系统调配 CPU,也就是说该线程得不到任何推动。

这并不是一种高效的做法。

没有一个程序员想死盯着屏幕 10 分钟后能力失去后果。

那么有没有一种更加高效的做法呢?

想一想咱们上一篇中那个始终盯着你写代码的老板 (见《从小白到高手,你须要了解同步与异步》),咱们曾经晓得了这种 始终期待直到另一个工作实现的 模式叫做同步。

如果你是老板的话你会什么都不干始终盯着员工写代码吗?因而一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后天然会告诉老板,这样老板和程序员都不须要互相期待,这种模式被称为异步。

回到咱们的主题,这里一种更好的形式是 调用 make_youtiao()这个函数后不再期待这个函数执行实现 ,而是间接返回持续后续流程,这样 A 小组的程序就能够和 make_youtiao() 这个函数同时进行了,就像这样:

在这种状况下,回调 (callback) 就必须出场了。

为什么咱们须要回调 callback

有的同学可能还没有明确为什么在这种状况下须要回调,别着急,咱们缓缓讲。

假如咱们“明日油条”App 代码第一版是这样写的:

make_youtiao(10000);
sell();

能够看到这是最简略的写法,意思很简略,制作好油条后卖出去。

咱们曾经晓得了因为 make_youtiao(10000)这个函数 10 分钟能力返回,你不想始终死盯着屏幕 10 分钟期待后果,那么一种更好的办法是让 make_youtiao()这个函数晓得制作完油条后该干什么,即,更好的调用 make_youtiao 的形式是这样的:“制作 10000 个油条,炸好后卖出去”,因而调用 make_youtiao 就变出这样了:

make_youtiao(10000, sell);

看到了吧,当初 make_youtiao 这个函数多了一个参数,除了指定制作油条的数量外 还能够指定制作好后该干什么,第二个被 make_youtiao 这个函数调用的函数就叫回调,callback。

当初你应该看进去了吧,尽管 sell 函数是你定义的,然而这个函数却是被其它模块调用执行的,就像这样:

make_youtiao 这个函数是怎么实现的呢,很简略:

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 吗?这个问题该怎么解决呢?对于这个问题的答案,关注公众号“码农的荒岛求生 ”并回复“ 改良”二字你就能晓得答案了。

异步回调

故事到这里还没有完结。

在下面的示例中,尽管咱们应用了回调这一概念,也就是调用方实现回调函数而后再将该函数当做参数传递给其它模块调用。

然而,这里仍然有一个问题,那就是 make_youtiao 函数的调用形式仍然是同步的,对于同步异步请参考《从小白到高手,你须要了解同步与异步》,也就是说调用方是这样实现的:

make_youtiao(10000, sell);
// make_youtiao 函数返回前什么都做不了

咱们能够看到,调用方必须期待 make_youtiao 函数返回后才能够持续后续流程,咱们再来看下 make_youtiao 函数的实现:

void make_youtiao(int num, func call_back) {real_make_youtiao(num);
    call_back(); // 执行回调}

看到了吧,因为咱们要制作 10000 个油条,make_youtiao 函数执行完须要 10 分钟,也就是说即使咱们应用了回调,调用方齐全不须要关怀制作完油条后的后续流程,然而调用方仍然会被阻塞 10 分钟,这就是同步调用的问题所在。

如果你真的了解了上一节的话应该能想到一种更好的办法了。

没错,那就是异步调用。

对于异步回调,请关注“码农的荒岛求生 ”并回复“ 回调”二字你就能晓得答案了。

新的编程思维模式

让咱们再来认真的看一下这个过程。

程序员最相熟的思维模式是这样的:

  1. 调用某个函数,获取后果
  2. 解决获取到的后果
res = request();
handle(res);

这就是函数的同步调用,只有 request()函数返回拿到后果后,能力调用 handle 函数进行解决,request 函数返回前咱们必须 期待,这就是同步调用,其控制流是这样的:

然而如果咱们想更加高效的话,那么就须要异步调用了,咱们不去间接调用 handle 函数,而是作为参数传递给 request:

request(handle);

咱们基本就不关怀 request 什么时候真正的获取的后果,这是 request 该关怀的事件,咱们只须要把获取到后果后该怎么解决通知 request 就能够了,因而 request 函数能够立即返回,真的获取后果的解决可能是在另一个线程、过程、甚至另一台机器上实现。

这就是异步调用,其控制流是这样的:

从编程思维上看,异步调用和同步有很大的差异,如果咱们把解决流程当做一个工作来的话,那么同步下整个工作都是咱们来实现的,然而异步状况下工作的解决流程被分为了两局部:

  1. 第一局部是咱们来解决的,也就是调用 request 之前的局部
  2. 第二局部不是咱们解决的,而是在其它线程、过程、甚至另一个机器上解决的。

咱们能够看到因为工作被分成了两局部,第二局部的调用不在咱们的掌控范畴内,同时只有调用刚才晓得该做什么,因而在这种状况下回调函数就是一种必要的机制了。

也就是说回调函数的实质就是“只有咱们才晓得做些什么,然而咱们并不分明什么时候去做这些,只有其它模块才晓得,因而咱们必须把咱们晓得的封装成回调函数通知其它模块”。

当初你应该能看出异步回调这种编程思维模式和同步的差别了吧。

接下来咱们给回调一个较为学术的定义

正式定义

在计算机科学中,回调函数是指一段以参数的模式传递给其它代码的可执行代码。

这就是回调函数的定义了。

回调函数就是一个函数,和其它函数没有任何区别。

留神,回调函数是一种软件设计上的概念,和某个编程语言没有关系,简直所有的编程语言都能实现回调函数。

对于个别的函数来说,咱们本人编写的函数会在本人的程序外部调用,也就是说函数的编写方是咱们本人,调用方也是咱们本人。

但回调函数不是这样的,尽管函数编写方是咱们本人,然而函数调用方不是咱们,而是咱们援用的其它模块,也就是第三方库,咱们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用咱们编写的回调函数,如图所示:

而之所以须要给第三方库指定回调函数,是因为第三方库的编写者并不分明在某些特定节点,比方咱们举的例子油条制作实现、接管到网络数据、文件读取实现等之后该做什么,这些只有库的应用刚才晓得,因而第三方库的编写者无奈针对具体的实现来写代码,而只能对外提供一个回调函数,库的应用方来实现该函数,第三方库在特定的节点调用该回调函数就能够了。

另一点值得注意的是,从图中咱们能够看出回调函数和咱们的主程序位于 同一层 中,咱们只负责编写该回调函数,但并不是咱们来调用的。

最初值得注意的一点就是回调函数被调用的工夫节点,回调函数只在某些特定的节点被调用,就像下面说的油条制作实现、接管到网络数据、文件读取实现等,这些都是事件,也就是 event,实质上咱们编写的回调函数就是用来解决 event 的,因而从这个角度看回调函数不过就是 event handler,因而回调函数人造实用于事件驱动编程 event-driven,咱们将会在后续文章中再次回到这一主题。

为什么异步回调这种思维模式正变得的越来越重要

在同步模式下,服务调用方会因服务执行而被阻塞暂停执行,这会导致整个线程被阻塞,因而这种编程形式人造不适用于高并发动辄几万几十万的并发连贯场景,

针对高并发这一场景,异步其实是更加高效的,起因很简略,你不须要在原地期待,因而从而更好的利用机器资源,而回调函数又是异步下不可或缺的一种机制。

回调天堂,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) {....});
        });
    });
});

我想不须要再强调什么了吧,你感觉这两种写法哪个更容易了解,代码更容易保护呢?

博主有幸已经保护过这种类型的代码,不得不说每次减少新性能的时候巴不得本人化为两个分身,一个不得不去重读一边代码;另一个在一旁骂本人为什么当初抉择保护这个我的项目。

异步回调代码稍不注意就会跌到回调陷阱中,那么有没有一种更好的方法既能联合异步回调的高效又能联合同步编码的简略易读呢?

侥幸的是,答案是必定的,关注公众号“码农的荒岛求生”并回复“8”你就晓得答案了。

总结

在这篇文章中,咱们从一个理论的例子登程具体解说了回调函数这种机制的前因后果,这是应答高并发、高性能场景的一种极其重要的编码机制,异步加回调能够充分利用机器资源,实际上异步回调最实质上就是事件驱动编程,这是咱们接下来要重点解说的内容。

退出移动版